1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsとSupabaseでオンライ学習プラットフォーム(LMS)を構築する | エピソード2: ユーザー認証システムの構築

Posted at

こんにちは!前回のエピソードでは、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認証を設定します。

  1. Supabaseダッシュボードでの設定

    • Supabaseダッシュボードにログイン。
    • 「Authentication」→「Providers」で以下を設定:
      • Email: 有効化(デフォルトでON)。
      • Google: 有効化し、Google Cloud ConsoleでOAuthクライアントIDとシークレットを生成後、入力。
    • 「Settings」→「Redirect URLs」にリダイレクトURLを追加(例: http://localhost:3000/api/auth/callback)。
  2. 必要なパッケージのインストール
    Supabase認証ヘルパーとフォームライブラリをインストール:

npm install @supabase/auth-helpers-nextjs react-hook-form

ステップ2: Supabase Authクライアントの設定

Supabaseの認証をNext.jsで管理するために、ヘルパーライブラリを設定します。

  1. 認証クライアントの初期化
    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 });
  1. 認証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を使って、登録とログインフォームを作成します。

  1. 登録ページ
    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>
  );
}
  1. ログインページ
    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: ダッシュボードとプロテクトルートの設定

認証済みユーザーのみアクセス可能なダッシュボードを作成し、プロテクトルートを実装します。

  1. ダッシュボードページ
    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>
  );
}
  1. ナビゲーションの更新
    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: 動作確認

  1. SupabaseダッシュボードでGoogle OAuth設定とリダイレクトURLが正しいことを確認。
  2. 開発サーバーを起動:
npm run dev
  1. http://localhost:3000にアクセスし、以下の点を確認:
    • ヘッダーに「ログイン」と「登録」リンクが表示される。
    • /registerでメールとパスワードを入力し、登録できる。
    • /loginでメールログインとGoogleログインが機能する。
    • ログイン後、/dashboardにリダイレクトされ、メールアドレスが表示される。
    • 未認証状態で/dashboardにアクセスすると、/loginにリダイレクトされる。
  2. ログアウトボタンをクリックし、セッションが終了することを確認。

エラーがあれば、Supabaseの認証ログや.env.localの設定を確認してください。


まとめと次のステップ

このエピソードでは、Supabase Authを使ってユーザー認証システムを構築しました。メールとGoogle OAuth認証、React Hook Formによるバリデーション、プロテクトルートを実装し、安全なユーザー体験の基盤を整えました。

次回のエピソードでは、Supabase Databaseを使ってコース管理機能を実装します。coursesテーブルの設計と、コース一覧ページ(CLP)の構築を行いますので、引き続きお楽しみに!


この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?