Next.js+Supabaseで認証機能を実装しよう【コード付き完全ガイド】

今回ご紹介するのは『Next.js』と『Supabase』を併用する場合、どのように認証機能を実装することができるか、についてです。
この記事にかかれているとおりに実装していただければ、安全に素早く認証機能を用意することが出来ます。

サインイン、サインアップだけでなく、パスワードを忘れた際のパスワード再発行機能を一から作成するので、『Next.js』や『認証機能』について勉強中の方も、ぜひこちらを読んで仕組みを学んでみてください。

目次

事前準備

①:Supabaseプロジェクトの作成

Supabaseにログイン(登録していないければアカウント登録から)して、
トップページの「New Project」を押します。

すると、下記の様な画面が表示されます。

適当なプロジェクト名とデータベースのパスワードを入れてあげてください。
Regionはできれば自分の住んでいる地域の近くがいいため、日本で設定するのがいいと思います。

②:Next.jsプロジェクトの作成

任意のディレクトリで、npx create-next-app -e with-supabaseと打ち込み、
対話的にアプリケーションの名前の設定をしてプロジェクトの作成を行いましょう。

次に環境変数の設定を行います。
作成されたNext.jsプロジェクトの直下に.env.local.exampleファイルがあるので、
このファイルを.env.localにリネームし、
SupabaseのサイトのProject Settings→APIにある、Project URLとAPI Keyをそれぞれ添付画像の変数に設定してください。

その後npm run devを実行して、下記のような画面が出ればプロジェクトの作成成功です。

認証機能の作成

①:必要なファイルの作成

今回作りたい画面に合わせてプロジェクトのファイル構成を変更しましょう。
現状のファイル構成が下記のようになっているかと思いますが、まずは不要なファイルを削除し、必要なファイルと入れ替えて行きましょう。

上記を下記の状態に変更します

削除するファイルは、
・app/auth/login/page.tsx
・componentsフォルダの直下すべて
・util/supabase直下すべて
になります。
ログインページ無くて大丈夫なの?と思うかもしれませんが、今回はモーダルでログインのUIを実現するため、ページとしては不要です。

作成するファイルは、
・app/auth/login/route.ts
・app/auth/logout/route.ts
・app/profile/page.tsx
・app/resetPassword/page.tsx
・app/resetPassword/inputPassword/page.tsx
・components/modalCore.tsx
・components/navigation.tsx
・components/supabaseListener.tsx
・components/modal/modalType.ts
・components/modal/signinForm.tsx
・components/modal/signupForm.tsx
・utils/supabase/supabase.ts

になります。(一旦ファイルを新規作成するのみで大丈夫です。中身は後で作ります)
多いので、一個ずつテキストを読みながら作成してみてください。

では、各ファイルの中身を作成する前に下準備から行いましょう。

②:下準備

Next.js Auth Helper Package

今回、認証機能をNext.js上で作成するにあたって、より便利に認証を扱うためにNext.js Auth Helper Packageを利用します。
これを利用することで、Next.jsのルーター(URLパス制御機能)上でどこにいてもセッションベースの認証機能が利用可能になります。
導入方法は下記のコマンドになります。

npm install @supabase/auth-helpers-nextjs

utils/supabase/supabase.ts

ユーザ登録時に必要になるため、事前にsupabase.tsの中身を作成します。

import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)

.env.localに入力したURLとAPIキーがここで利用されます。

③:認証周りのルーティング

認証周りのルーティングのためにapp/authに入っているroute.tsファイル群を作成していきます。

app/auth/callback/route.ts

import { NextResponse } from 'next/server'

import type { NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
    const requestUrl = new URL(request.url)

    console.log(requestUrl.origin)
    return NextResponse.redirect(requestUrl.origin + '/')
}

こちらはユーザ登録後に送られるメールアドレス上に載っているURLにアクセスすることで発火するGETになります。
内容としてはシンプルにトップページにリダイレクトする形です。
あくまでユーザ登録確認が行えればいいため、この実装にしています。

app/auth/login/route.ts

import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
    const requestUrl = new URL(request.url)
    const formData = await request.formData()
    const email = String(formData.get('email'))
    const password = String(formData.get('password'))
    const supabase = createRouteHandlerClient({ cookies })

    await supabase.auth.signInWithPassword({
        email,
        password,
    })

    return NextResponse.redirect(requestUrl.origin + '/profile', {
        status: 301,
    })
}

ログインフォームに入力し、Submitが行われるとこちらの処理に飛びます。
ログインに利用するメールアドレス、パスワードをsupabase側のsignin処理に送っています。
問題がなければそのままプロフィール画面が表示されます。

app/auth/logout/route.ts

import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
    const requestUrl = new URL(request.url)
    const supabase = createRouteHandlerClient({ cookies })

    await supabase.auth.signOut()

    return NextResponse.redirect(requestUrl.origin + '/', {
        status: 301,
    })
}

こちらはプロフィール画面でログアウトを行った際に行われる処理です。
supabase側のsignout処理を行い、トップページにリダイレクトします。

④:基本ページ作成

トップページのメイン要素や基本のレイアウトを作成します。

app/page.tsx

export default function Home() {
  return (
    <main className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-16 pt-20 text-center lg:pt-32">
      <h1 className="mx-auto max-w-4xl font-display text-5xl font-medium tracking-tight text-slate-900 sm:text-7xl">
        Hello
        <span className="relative whitespace-nowrap text-blue-600"> World</span>
      </h1>
    </main>
  )
}

app/layout.tsx

import './globals.css'
import SupabaseListener from '@/components/supabaseListener'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ja">
      <body>
        <div>
          <SupabaseListener />
        </div>
        <main>{children}</main>
      </body>
    </html>
  )
}

トップページの見た目は下記のようになります。

⑤:ユーザ登録、ログインフォーム

モーダルの共通部分と、ユーザ登録、ログインそれぞれのフォーム、加えて各フォームに切り替えるために利用する列挙型をそれぞれ作成します。

components/modalCore.tsx

import { useState } from 'react';
import { ModalType } from './modal/modalType';
import SignUpForm from './modal/signupForm';
import SignInForm from './modal/signinForm';

interface Props {
  modalType: ModalType;
}

const ModalCore = ({ modalType }: Props) => {
  const [showModal, setShowModal] = useState(false);
  let title = "";
  let headerButton = "";
  let formElement = <p>フォームを読み込めませんでした。</p>;
  switch (modalType) {
    case ModalType.SignIn:
      title = "ログインフォーム";
      headerButton = "Login";
      formElement = <SignInForm showModal={setShowModal}></SignInForm>;
      break;

    case ModalType.SignUp:
      title = "ユーザ登録フォーム";
      headerButton = "Sign Up";
      formElement = <SignUpForm showModal={setShowModal}></SignUpForm>;
      break;
  }
  return (
    <>
      <button
        className="text-gray-600 hover:text-blue-600"
        type="button"
        onClick={() => setShowModal(true)}
      >
        {headerButton}
      </button>
      {showModal ? (
        <>
          <div className="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 max-h-full bg-black-rgba">
            <div className="m-auto relative p-4 w-full max-w-md max-h-full">
              <div className="relative bg-white rounded-lg shadow">
                <div className="flex items-center justify-between p-4 md:p-5 border-b rounded-t">
                  <h3 className="text-xl font-semibold text-gray-900">
                    {title}
                  </h3>
                  <button
                    type="button"
                    className="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center"
                    data-modal-hide="authentication-modal"
                    onClick={() => setShowModal(false)}
                  >
                    <svg
                      className="w-3 h-3"
                      aria-hidden="true"
                      xmlns="http://www.w3.org/2000/svg"
                      fill="none"
                      viewBox="0 0 14 14"
                    >
                      <path
                        stroke="currentColor"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        strokeWidth="2"
                        d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
                      />
                    </svg>
                    <span className="sr-only">モーダルを閉じる</span>
                  </button>
                </div>
                <div className="p-4 md:p-5">{formElement}</div>
              </div>
            </div>
          </div>
        </>
      ) : null}
    </>
  );
};

export default ModalCore;

モーダルの共通部分になります。
ユーザ登録、ログインフォームの切り替えやモーダル自身のon/off切り替えの機能が実装されています。

フォームの切り替えは引数として渡されたmodalTypeの情報をもとに行っています。

  switch (modalType) {
    case ModalType.SignIn:
      title = "ログインフォーム";
      headerButton = "Login";
      formElement = <SignInForm showModal={setShowModal}></SignInForm>;
      break;

    case ModalType.SignUp:
      title = "ユーザ登録フォーム";
      headerButton = "Sign Up";
      formElement = <SignUpForm showModal={setShowModal}></SignUpForm>;
      break;
  }

モーダルのon/offは下記の状態管理で行っています。

const [showModal, setShowModal] = useState(false);

components/modal/signupForm.tsx

"use client";
import { supabase } from "@/utils/supabase/supabase";
import { useState } from "react";
import { Dispatch, SetStateAction } from "react";

export default function SignUpForm(props: {
  showModal: Dispatch<SetStateAction<boolean>>;
}) {
  const { showModal } = props;
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [passwordConf, setPasswordConf] = useState("");

  const onSubmit = async (event: any) => {
    event.preventDefault();
    showModal(false);
    try {
      const { error: signUpError } = await supabase.auth.signUp({
        email: email,
        password: password,
        options: {
          emailRedirectTo: `${location.origin}/auth/callback`,
        },
      });
      if (signUpError) {
        throw signUpError;
      }
      alert("登録完了メールを確認してください");
    } catch (error) {
      alert("エラーが発生しました");
    }
  };

  return (
    <form className="space-y-4" onSubmit={onSubmit}>
      <div>
        <label
          htmlFor="email"
          className="block mb-2 text-sm font-medium text-gray-900"
        >
          メールアドレス
        </label>
        <input
          type="email"
          name="email"
          id="email"
          className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
          placeholder="name@company.com"
          required
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </div>
      <div>
        <label
          htmlFor="password"
          className="block mb-2 text-sm font-medium text-gray-900"
        >
          パスワード
        </label>
        <input
          type="password"
          name="password"
          id="password"
          placeholder="••••••••"
          className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
          required
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </div>
      <div>
        <label
          htmlFor="passwordConf"
          className="block mb-2 text-sm font-medium text-gray-900"
        >
          パスワード(確認)
        </label>
        <input
          type="password"
          name="passwordConf"
          id="passwordConf"
          placeholder="••••••••"
          className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
          required
          value={passwordConf}
          onChange={(e) => setPasswordConf(e.target.value)}
        />
      </div>
      <div>
        <button
          type="submit"
          className="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
        >
          サインアップ
        </button>
      </div>
    </form>
  );
}

ユーザ登録フォームになります。
親(モーダルの共通部分)の表示on/offをフォーム側の処理に合わせて行うため、showModalを引数に渡しています。
また、
・email
・パスワード
・パスワード確認
を状態管理しており、ユーザ登録処理に利用しています。

  const { showModal } = props;
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [passwordConf, setPasswordConf] = useState("");

フォームが送信されたとき、下記のようにモーダルを閉じて、supabase側のユーザ登録処理を行い、/auth/callbackのルーティングへのリンクを持つメールが送信されます。

    showModal(false);
    try {
      const { error: signUpError } = await supabase.auth.signUp({
        email: email,
        password: password,
        options: {
          emailRedirectTo: `${location.origin}/auth/callback`,
        },
      });

ユーザ登録フォームの見た目は下記のような形です。

signup処理が成功すると、Supabaseのダッシュボード→Authenticationにユーザが追加されるのがわかります。
ただ、waiting for verificationとなっており、メールを確認しないと本登録扱いになりません。

メール確認後は以下のようにサインイン日時が登録されます

components/modal/signinForm.tsx

import { Dispatch, SetStateAction } from "react";
import Link from 'next/link';

export default function SignInForm(props: {
  showModal: Dispatch<SetStateAction<boolean>>;
}) {
  const { showModal } = props;
  return (
    <form action="/auth/login" method="post" className="space-y-4">
      <div>
        <label
          htmlFor="email"
          className="block mb-2 text-sm font-medium text-gray-900"
        >
          メールアドレス
        </label>
        <input
          type="email"
          name="email"
          id="email"
          className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
          placeholder="name@company.com"
          required
        />
      </div>
      <div>
        <label
          htmlFor="password"
          className="block mb-2 text-sm font-medium text-gray-900"
        >
          パスワード
        </label>
        <input
          type="password"
          name="password"
          id="password"
          placeholder="••••••••"
          className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
          required
        />
      </div>
      <div className="text-right">
        <Link className="font-medium text-blue-600 dark:text-blue-500 hover:underline" href={`${location.origin}/resetPassword`} onClick={() => showModal(false)}>パスワードを忘れた場合</Link>
      </div>
      <div>
        <button className="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">
          サインイン
        </button>
      </div>
    </form>
  );
}

ログインフォームの場合も同じようにshowModalが引数に渡されていますが、内部的な処理はすべてlogin/route.ts側にPOSTされます。

ログインフォームの見た目は下記のような形です。

components/modal/modalType.tsx

export enum ModalType {
  SignIn = 1,
  SignUp = 2,
}

シンプルに列挙型でSignInとSignUpの2つを定義しています。
フォームの切り替えに利用しています。

tailwind.config.js

フォームが開かれたときに背景を暗くするため、tailwindのコンフィグファイルにカスタムCSSを作成します。

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      colors: {
        'black-rgba': 'rgba(0, 0, 0, 0.3)',
      },
    },
  },
  plugins: [],
}

black-rgbaで定義している部分になります。

⑥:下層ページ作成

『プロフィールページ』、及び『パスワードを忘れた場合のメールアドレス入力ページ』、『パスワード再発行ページ』の作成を行います。

app/profile/page.tsx

"use client"
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";
import { useEffect } from "react";

/**
 * ログイン後のマイページ
 */
const MyPage = () => {
    const supabase = createClientComponentClient();
    useEffect(() => {
        async function getData() {
            const { data } = await supabase.auth.getSession();
            console.log(data);
            // ...
        }
        getData();
    }, []);

    return (
        <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-16 pt-20 text-center lg:pt-32">
            <h1 className="text-2xl font-bold">
                ログインに成功しました
            </h1>
            <div className="pt-10">
                <form action="/auth/logout" method="post">
                    <button 
                     className=" text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
                     type="submit"
                     >
                        ログアウト
                    </button>
                </form>
            </div>
        </div>
    )
}


export default MyPage;

シンプルに『ログイン成功』の表示と『ログアウト』のみができるページになっています。
必要であればgetDataに更に処理を追加し、よりコンテンツを充実させてください。そうすれば、しっかりとしたプロフィールページを作ることが出来ます。

見た目は下記のような形です。

app/resetPassword/page.tsx

"use client"
import { useState } from 'react';
import { AuthError } from '@supabase/supabase-js';
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";


/**
 * パスワードリセット前のメール送信用画面
 */
const ResetPassword = () => {
  const [email, setEmail] = useState("");
  const [isSend, setIsSend] = useState(false);
  const [error, setError] = useState<AuthError | null>(null);
	 
  const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    try {
      const supabase = createClientComponentClient()
      const { error } = await supabase.auth.resetPasswordForEmail(email, {
        redirectTo: `${location.origin}/resetPassword/inputPassword`,
      });
      if (error) {
        setError(error);
	    throw error;
      }
      setIsSend(true);
    } catch (error) {
      console.log(error);
    }
  };
  
  if (error) {
    return (
        <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-16 pt-20 text-center lg:pt-32">
             <p>エラーが発生しました</p>
        </div>
    )
  }
  
  if (isSend) {
    return (
        <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-16 pt-20 text-center lg:pt-32">
             <p>メールを送信しました</p>
        </div>
    )
  }
  
  return (
    <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-16 pt-20 text-center lg:pt-32">
     <p>アカウントに結びついているメールアドレスを入力してください</p>
     <form className="pt-10" onSubmit={onSubmit}>
        <input className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 md:w-2/3 lg:w-1/2 p-2.5" value={email} type="email" onChange={(e) => setEmail(e.target.value)} placeholder="メールアドレス" />
        <button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 ml-2 text-center" type="submit">送信</button>
      </form>
    </div>
  );
}

export default ResetPassword

パスワードリセット処理の、一番初めにたどり着くメール送信ページになります。

  const [email, setEmail] = useState("");
  const [isSend, setIsSend] = useState(false);
  const [error, setError] = useState<AuthError | null>(null);

上記のように、
・入力したメールアドレス
・メールに送信できたかどうか
・エラーが発生したかどうか
をそれぞれ状態管理し、supabaseのリセットメール送信処理と、その後のページ更新に利用しています。

リセットメール送信処理

      const { error } = await supabase.auth.resetPasswordForEmail(email, {
        redirectTo: `${location.origin}/resetPassword/inputPassword`,
      });

ページ更新(送信できたorエラー)

  if (error) {
    return (
        <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-16 pt-20 text-center lg:pt-32">
             <p>エラーが発生しました</p>
        </div>
    )
  }
  
  if (isSend) {
    return (
        <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-16 pt-20 text-center lg:pt-32">
             <p>メールを送信しました</p>
        </div>
    )
  }

ページの見た目は以下のようになります。

送信後:

app/resetPassword/inputPassword/page.tsx

"use client"
import { useState } from 'react';
import { AuthError } from '@supabase/supabase-js';
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";

/**
 * パスワードリセット用画面
 */
const InputPasswordForReset = () => {
  const [password, setPassword] = useState("");
  const [passwordConf, setPasswordConf] = useState("");
  const [isSend, setIsSend] = useState(false);
  const [error, setError] = useState<AuthError | null>(null);
	 
  const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    try {
      const supabase = createClientComponentClient()
      const { error } = await supabase.auth.updateUser({ password: password });
      if (error) {
        setError(error);
	    throw error;
      } 
      setIsSend(true);
    } catch (error) {
      console.log(error);
    }
  };
  
  if (error) {
    return (
        <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-16 pt-20 text-center lg:pt-32">
             <p>エラーが発生しました</p>
        </div>
    )
  }
  
  if (isSend) {
    return (
        <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-16 pt-20 text-center lg:pt-32">
             <p>パスワードを更新しました</p>
        </div>
    )
  }
  
  return (
    <div className="mx-auto max-w-7xl md:w-1/2 lg:w-1/4 px-4 sm:px-6 lg:px-8 pb-16 pt-20 text-center lg:pt-32">
     <p>新しいパスワードを入力してください</p>
     <form className="pt-10 text-left" onSubmit={onSubmit}>
        <div>
            <label
            htmlFor="password"
            className="block mb-2 text-sm font-medium text-gray-900"
            >
            パスワード
            </label>
            <input
            type="password"
            name="password"
            id="password"
            placeholder="••••••••"
            className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
            required
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            />
      </div>
      <div className='pt-5'>
        <label
          htmlFor="passwordConf"
          className="block mb-2 text-sm font-medium text-gray-900"
        >
          パスワード(確認)
        </label>
        <input
          type="password"
          name="passwordConf"
          id="passwordConf"
          placeholder="••••••••"
          className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
          required
          value={passwordConf}
          onChange={(e) => setPasswordConf(e.target.value)}
        />
        </div>
        <div className='text-center mt-5'>
        <button className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-10 py-2.5 text-center"
         type="submit">送信</button>

        </div>
      </form>
    </div>
  );
}

export default InputPasswordForReset

リセットメール送信後、そこから遷移してくるパスワード再発行ページです。

  const [password, setPassword] = useState("");
  const [passwordConf, setPasswordConf] = useState("");
  const [isSend, setIsSend] = useState(false);
  const [error, setError] = useState<AuthError | null>(null);

上記のように
・パスワード
・パスワード確認
・送信できたかどうか
・エラーが発生したかどうか
をそれぞれ状態管理し、supabaseのユーザ情報更新処理と、その後のページ更新(リセット時とほぼ同じ為省略)に利用しています。

supabaseのユーザ情報更新処理

const { error } = await supabase.auth.updateUser({ password: password });

見た目は以下のようになります。

⑦:ナビゲーション(ヘッダー)

各ページは作成できましたが、『それぞれのページへアクセスするためのナビゲーション』と『認証機能を利用するため、セッション情報をナビゲーションに与える部分』の実装がされていません。
下記のように作成してください。

components/navigation.tsx

'use client';
import type { Session } from '@supabase/auth-helpers-nextjs';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import ModalCore from './modalCore';
import { ModalType } from './modal/modalType';
const Navigation = ({ session }: { session: Session | null }) => {
  const pathname = usePathname();
  const router = useRouter();
  if (session === null && pathname?.includes('/profile')) {
    router.push('/');
  }
  return (
    <header>
      <div className="flex items-center justify-between px-4 py-2 bg-white shadow-md">
        <nav className="hidden md:flex space-x-4">
          <div>
            <Link className="text-gray-600 hover:text-blue-600" href="/">
              Home
            </Link>
          </div>
          {session ? (
            <div>
              <Link
                className="text-gray-600 hover:text-blue-600"
                href="/profile"
              >
                Profile
              </Link>
            </div>
          ) : (
            <>
              <div>
                <ModalCore modalType={ModalType.SignIn}></ModalCore>
              </div>
              <div>
                <ModalCore modalType={ModalType.SignUp}></ModalCore>
              </div>
            </>
          )}
        </nav>
      </div>
    </header>
  )
}

export default Navigation

各ページへのナビゲーション部分。ページのUI上はヘッダーになります。

各ページやモーダルへのアクセスを行うための記述です。
更に加えて、プロフィールページへ『セッション情報無し』でアクセスした際に、トップページへリダイレクトさせる処理も実装されています。

  if (session === null && pathname?.includes('/profile')) {
    router.push('/');
  }

components/supabaseListener.tsx

import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"
import { cookies } from "next/headers"
import Navigation from "./navigation"

const SupabaseListener = async () => {
    const supabase = createServerComponentClient({ cookies })

    const {
        data: { session },
    } = await supabase.auth.getSession()

    return <Navigation session={session} />
}

export default SupabaseListener;

上記でnavigation.tsxにセッション情報を渡しています。

これでNext.js+Supabaseの認証機能の作成は完了です!
npm run devしてちゃんとユーザ登録、ログイン、パスワード再設定が動いていることを確認してみてください。
よりSupabaseへの理解が深まると思います。

ここまで掲載したコードについては、github上でも公開しております。
万一動かない場合等は、ご自身の構成と見比べてみてください。

なおTodoONada株式会社では、Supabaseやシステム開発等についてのノウハウ記事を数々ご用意しています。
ぜひこちらも御覧ください!

あわせて読みたい
素早くアプリを作りたい! Vercel+Supabaseでプロジェクト開発 ここ数年、『アプリケーションの開発』はだいぶハードルが低いものになってきました。かつては学校で学ばなければ書けなかったようなプログラミング知識も、今はネット...
あわせて読みたい
Supabaseの機能一覧・解説 オープンソースのFirebase代替mBaaS 前回『Firebaseの機能一覧』についてご紹介しましたが、『機能が多すぎる』と感じた方もいらっしゃるでしょう。もっとシンプルに、安く、使いやすいmBaaSはないのでしょ...

お問合せ&各種リンク

presented by 

よかったらシェアしてね!
  • URLをコピーしました!
目次