こんにちは!前回のエピソードでは、Next.jsとSupabaseを使ってLMSプロジェクトの環境をセットアップし、シンプルなホームページを作成しました。今回は、Supabase Authを活用してユーザー認証システムを構築します。メールアドレスでの登録・ログイン、Google OAuth認証、フォームバリデーション(React Hook Form)、およびプロテクトルートの設定を行います。これで、ユーザーが安全にアプリを利用できる基盤が整います!
このエピソードのゴール
- Supabase AuthでメールとGoogle OAuth認証を実装。
- React Hook Formを使った登録・ログインフォームを構築。
- Supabaseクライアントでユーザーセッションを管理。
- Next.jsでプロテクトルートを設定。
必要なもの
- 前回のプロジェクト(
next-lms
)がセットアップ済み。 - Supabaseプロジェクト(認証設定済み)。
-
react-hook-form
と@supabase/auth-helpers-nextjs
パッケージ。 - 基本的なTypeScript、React、Next.jsの知識。
ステップ1: Supabase Authの設定
Supabase Authを有効化し、メールとGoogle OAuth認証を設定します。
-
Supabaseダッシュボードでの設定:
- Supabaseダッシュボードにログイン。
- 「Authentication」→「Providers」で以下を設定:
- Email: 有効化(デフォルトでON)。
- Google: 有効化し、Google Cloud ConsoleでOAuthクライアントIDとシークレットを生成後、入力。
- 「Settings」→「Redirect URLs」にリダイレクトURLを追加(例:
http://localhost:3000/api/auth/callback
)。
-
必要なパッケージのインストール:
Supabase認証ヘルパーとフォームライブラリをインストール:
npm install @supabase/auth-helpers-nextjs react-hook-form
ステップ2: Supabase Authクライアントの設定
Supabaseの認証をNext.jsで管理するために、ヘルパーライブラリを設定します。
-
認証クライアントの初期化:
src/lib/supabase.ts
を更新して、サーバーとクライアントの両方で使えるクライアントを準備:
import { createClient } from '@supabase/supabase-js';
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
// クライアントサイド用
export const supabase = createClient(supabaseUrl, supabaseKey);
// サーバーコンポーネント用
export const supabaseServer = () =>
createServerComponentClient({ cookies }, { supabaseUrl, supabaseKey });
-
認証APIルートの作成:
src/app/api/auth/callback/route.ts
を作成して、OAuthリダイレクトを処理:
import { NextResponse } from 'next/server';
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
if (code) {
const supabase = createRouteHandlerClient({ cookies }, {
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL!,
supabaseKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
});
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect(new URL('/dashboard', request.url));
}
このコードは、OAuth認証後のコールバックを処理し、ユーザーをダッシュボードにリダイレクトします。
ステップ3: 登録・ログインフォームの構築
React Hook Formを使って、登録とログインフォームを作成します。
-
登録ページ:
src/app/register/page.tsx
を作成:
import { useForm } from 'react-hook-form';
import { supabase } from '@/lib/supabase';
import { useRouter } from 'next/navigation';
type RegisterForm = {
email: string;
password: string;
};
export default function Register() {
const { register, handleSubmit, formState: { errors } } = useForm<RegisterForm>();
const router = useRouter();
const onSubmit = async (data: RegisterForm) => {
const { error } = await supabase.auth.signUp({
email: data.email,
password: data.password,
});
if (error) {
alert(error.message);
} else {
router.push('/dashboard');
}
};
return (
<main className="container mx-auto p-4 max-w-md">
<h1 className="text-3xl font-bold mb-6">登録</h1>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label className="block text-sm font-medium">メールアドレス</label>
<input
type="email"
{...register('email', { required: 'メールアドレスは必須です' })}
className="w-full border p-2 rounded"
/>
{errors.email && <p className="text-red-500 text-sm">{errors.email.message}</p>}
</div>
<div>
<label className="block text-sm font-medium">パスワード</label>
<input
type="password"
{...register('password', { required: 'パスワードは必須です', minLength: { value: 6, message: '6文字以上必要です' } })}
className="w-full border p-2 rounded"
/>
{errors.password && <p className="text-red-500 text-sm">{errors.password.message}</p>}
</div>
<button type="submit" className="bg-primary text-white px-6 py-3 rounded hover:bg-opacity-90">
登録
</button>
</form>
<p className="mt-4">
すでにアカウントをお持ちですか?{' '}
<a href="/login" className="text-primary hover:underline">ログイン</a>
</p>
</main>
);
}
-
ログインページ:
src/app/login/page.tsx
を作成:
import { useForm } from 'react-hook-form';
import { supabase } from '@/lib/supabase';
import { useRouter } from 'next/navigation';
type LoginForm = {
email: string;
password: string;
};
export default function Login() {
const { register, handleSubmit, formState: { errors } } = useForm<LoginForm>();
const router = useRouter();
const onSubmit = async (data: LoginForm) => {
const { error } = await supabase.auth.signInWithPassword({
email: data.email,
password: data.password,
});
if (error) {
alert(error.message);
} else {
router.push('/dashboard');
}
};
const signInWithGoogle = async () => {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: 'http://localhost:3000/api/auth/callback' },
});
if (error) alert(error.message);
};
return (
<main className="container mx-auto p-4 max-w-md">
<h1 className="text-3xl font-bold mb-6">ログイン</h1>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label className="block text-sm font-medium">メールアドレス</label>
<input
type="email"
{...register('email', { required: 'メールアドレスは必須です' })}
className="w-full border p-2 rounded"
/>
{errors.email && <p className="text-red-500 text-sm">{errors.email.message}</p>}
</div>
<div>
<label className="block text-sm font-medium">パスワード</label>
<input
type="password"
{...register('password', { required: 'パスワードは必須です' })}
className="w-full border p-2 rounded"
/>
{errors.password && <p className="text-red-500 text-sm">{errors.password.message}</p>}
</div>
<button type="submit" className="bg-primary text-white px-6 py-3 rounded hover:bg-opacity-90">
ログイン
</button>
</form>
<button
onClick={signInWithGoogle}
className="mt-4 bg-gray-800 text-white px-6 py-3 rounded hover:bg-opacity-90 w-full"
>
Googleでログイン
</button>
<p className="mt-4">
アカウントをお持ちでないですか?{' '}
<a href="/register" className="text-primary hover:underline">登録</a>
</p>
</main>
);
}
これらのコードは:
- React Hook Formでフォームバリデーションを実装。
- Supabase AuthでメールとGoogle認証を処理。
- 成功時にダッシュボードにリダイレクト。
ステップ4: ダッシュボードとプロテクトルートの設定
認証済みユーザーのみアクセス可能なダッシュボードを作成し、プロテクトルートを実装します。
-
ダッシュボードページ:
src/app/dashboard/page.tsx
を作成:
import { supabaseServer } from '@/lib/supabase';
import { redirect } from 'next/navigation';
export default async function Dashboard() {
const supabase = supabaseServer();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
redirect('/login');
}
return (
<main className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">ダッシュボード</h1>
<p>ようこそ、{user.email}さん!</p>
<form
action={async () => {
'use server';
await supabase.auth.signOut();
redirect('/login');
}}
>
<button className="mt-4 bg-red-500 text-white px-6 py-3 rounded hover:bg-opacity-90">
ログアウト
</button>
</form>
</main>
);
}
-
ナビゲーションの更新:
src/app/layout.tsx
のヘッダーにログイン・ログアウトリンクを追加:
import '../styles/globals.css';
import Link from 'next/link';
import { supabaseServer } from '@/lib/supabase';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const supabase = supabaseServer();
const { data: { user } } = await supabase.auth.getUser();
return (
<html lang="ja">
<body className="bg-gray-100">
<header className="bg-primary text-white p-4">
<div className="container mx-auto flex justify-between items-center">
<h1 className="text-2xl font-bold">
<Link href="/">Next.js LMS</Link>
</h1>
<nav>
{user ? (
<>
<Link href="/dashboard" className="mr-4 hover:underline">ダッシュボード</Link>
<form
action={async () => {
'use server';
await supabase.auth.signOut();
}}
className="inline"
>
<button className="hover:underline">ログアウト</button>
</form>
</>
) : (
<>
<Link href="/login" className="mr-4 hover:underline">ログイン</Link>
<Link href="/register" className="hover:underline">登録</Link>
</>
)}
</nav>
</div>
</header>
{children}
</body>
</html>
);
}
このコードは:
- 認証状態に応じてナビゲーションを動的に表示。
- 未認証ユーザーを
/login
にリダイレクト。 - ログアウト機能をサーバーアクションで実装。
ステップ5: 動作確認
- SupabaseダッシュボードでGoogle OAuth設定とリダイレクトURLが正しいことを確認。
- 開発サーバーを起動:
npm run dev
-
http://localhost:3000
にアクセスし、以下の点を確認:- ヘッダーに「ログイン」と「登録」リンクが表示される。
-
/register
でメールとパスワードを入力し、登録できる。 -
/login
でメールログインとGoogleログインが機能する。 - ログイン後、
/dashboard
にリダイレクトされ、メールアドレスが表示される。 - 未認証状態で
/dashboard
にアクセスすると、/login
にリダイレクトされる。
- ログアウトボタンをクリックし、セッションが終了することを確認。
エラーがあれば、Supabaseの認証ログや.env.local
の設定を確認してください。
まとめと次のステップ
このエピソードでは、Supabase Authを使ってユーザー認証システムを構築しました。メールとGoogle OAuth認証、React Hook Formによるバリデーション、プロテクトルートを実装し、安全なユーザー体験の基盤を整えました。
次回のエピソードでは、Supabase Databaseを使ってコース管理機能を実装します。courses
テーブルの設計と、コース一覧ページ(CLP)の構築を行いますので、引き続きお楽しみに!
この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!