9
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 14 App router での Supabaseの認証ライブラリSSRの調査 最小限

Last updated at Posted at 2023-11-21

※注意
2023年12月28日現在
Supabaseの認証ライブラリSSRはベータ版です。

supabase/auth-helpers: A collection of framework specific Auth utilities for working with Supabase.

この記事のリポジトリ

masakinihirota/next-supabase-auth-ssr

目的

Supabaseの認証ライブラリSSRの調査

"@supabase/ssr": "latest",

Next.jsの公式サンプルの 'with-supabase'は以前からありましたが、その認証ライブラリは最近になって変更されました。

next.js/examples/with-supabase at canary · vercel/next.js

その調査をします。

↑のライブラリで不要なファイルは極力削除しています。
SupabaseでのSSR認証で関係があるファイルのみ残しています。

クライアントには2種類あります。
Supabaseを操作するためのプログラムと
ブラウザ上を指すクライアントです。

認証ではサーバー側かクライアント(ブラウザ上)側でSupabaseのクライアントを作り、
その作ったクライアントに、
SupabaseのURLと
Supabaseのアクセスキー
を渡します。

クライアント側ではクッキーを保存したり削除したりが自動化されていますが、
RSCではクッキーの設定や削除は自分で行う必要があります。
サーバー側では、ブラウザに設定したり、取得したり、削除する操作が必要です。
set()
get()
remove()
を作ります。

そもそも認証は
セッションの開始から終了までです。

セッションとは、
ユーザーがアプリケーションにログインしてからログアウトするまでの一連のインタラクションを指します。セッションは、ユーザーがアプリケーションを使用する間、その状態を維持するために使用されます。

セッションを開始して
セッションIDを作成します。
ブラウザを識別するキーを作り、それをクライアント=ブラウザ上=クッキーにセッションIDを保存します。

サーバー側は、ブラウザ側に発行したセッションIDがあれば、本物と認識します。
同じセッションIDがある確率が数百万分の1になるように設計されているからです。

※ここではセキュリティを考慮に入れていません。
セッションIDの横取りなど


Supabase Auth は、アクセス トークンとリフレッシュ トークンを使用する JWT ベースのアプローチをします。

セッションとは何ですか?

セッションは、ユーザーがサインインしたときに作成されます。
デフォルトではセッションは無期限に続き、ユーザはデバイスの数だけ無制限にアクティブなセッションを持つことができます。

セッションは、JWT形式のSupabase Authアクセストークンと、一意の文字列であるリフレッシュトークンで表されます。

アクセストークン

リフレッシュトークン

アクセストークンは通常5分から1時間程度の短い有効期限に設計されていますが、リフレッシュトークンは有効期限がなく、一度しか使用できません。

新しいアクセストークンとリフレッシュトークンのペアを取得するために、一度だけリフレッシュトークンを交換することができます。

このプロセスをセッションのリフレッシュと呼びます。

セッションの終了

セッションは、設定によって、以下の場合に終了します:

ユーザがサインアウトをクリックしたとき。

ユーザーがパスワードを変更するか、セキュリティ上重要な操作を行ったとき。

非アクティブによるタイムアウトのとき。

セッションの有効期間が最大になったとき。

ユーザーが別のデバイスでサインインしたとき。

アクセストークン(JWT)のクレーム

すべてのアクセストークンには、ユーザーのセッションを一意に識別するUUIDであるsession_id claimが含まれています。この ID は auth.sessions テーブルの主キーと関連付けることができます。

サインアウトとスコープ
Supabase Authでは、ユーザがアプリケーションでサインアウトAPIを呼び出す際に、3つの異なるスコープを指定することができます:

global (デフォルト): ユーザーがアクティブな全てのセッションを終了する。

local: ユーザの現在のセッションのみを終了し、他のデバイスやブラウザのセッションは有効なままにします。

others: ユーザーの現在のセッション以外を終了します。

// defaults to the global scope
await supabase.auth.signOut()

// sign out from the current session only
await supabase.auth.signOut({ scope: 'local' })

サインアウトすると、影響を受けるセッションに関連するすべてのリフレッシュ・トークンや潜在的に他のデータベース・オブジェクトは破棄され、クライアント・ライブラリはブラウザに保存されているセッションが削除されます。

ユーザーがアプリを使い続けると、リフレッシュ トークンが新しいアクセス トークンと常に交換されます。

User Sessions | Supabase Docs


Understand the Supabase SSR Package easily - YouTube

SupabaseのSSR認証はCookeiを使った認証を扱うライブラリです。
Supabaseクライアントを作り、Cookeiを操作します。

npm i @supabase/ssr

SupabaseのSSR認証のライブラリクライアント
基本的に使うのは2種類だけ
import { createBrowserClient } from '@supabase/ssr'
import { createServerClient } from '@supabase/ssr'

こちらはSupabaseのクライアント
import { createClient } from '@supabase/supabase-js'

import { createBrowserClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export function createSupabaseFrontendClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
  )
}

export function createSupabaseAppServerClient(serverComponent = false) {
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
    {
      cookies: {
        get(name) {
          //return cookie with the name 'name' here
          return cookies().get(name)?.value
        },
        set(name, value, options) {
          //set the cookie
          if (serverComponent) return
          return cookies().set(name, value, options)

        },
        remove(name, options) {
          //remove the cookie
          return cookies().set(name, '', options)
        },
      },
    }
  )
}

exprot function createSupabaseServerComponent(){
  return createSupabaseAppServerClient(true)
}

middleware.ts
import { NextResponse } from "next/server";
export async function middleware(req){
  const res = NextResponse.next()
  return res
}

npm install --save cookies-next

supabaseReqResClient.js
import { getCookie, setCookie } from 'cookies-next'

export function createSupabaseReqResClient(req, res){
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
    {
      cookies: {
        get(name) {
          //return cookie with the name 'name' here
          return getCookie(name, {req, res })
        },
        set(name, value, options) {
          //set the cookie
          setCookei(name, value, {req, res, ...options})
        },
        remove(name, options) {
          //remove the cookie
          if (serverComponent) return
          cookees().set(name, '', options)
        },
      },
    }
  )
  )
}


フレームワークに依存しない説明
4:30

Cookeの操作はSupabaseクライアントから
get
set
remove
の3つを使い操作します。

SupabaseのSSR認証の大枠を理解します。

process.env.NEXT_PUBLIC_SUPABASE_URL
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
環境変数で作ります。

後は
ブラウザ側で必要なCookeiの操作が必要です。

クッキーを
get(取得)
set(設定)
remove(削除)
基本的にこの3種類の操作があれば十分です。

これを設定する必要があります。

以上で終了です。

設定の仕方、そのコードの書き方はサンプルを見てください。


認証のためにSupabaseクライアントが必要です。

Supabaseクライアントには
Serverクライアント
Middlewareクライアント
Browserクライアント
の3種類があります。

基本的にすべて同じですが、サーバー、ミドルウェア、クライアントそれぞれの場所で多少の違いがあります。



環境

Windows10
node -v18.17.1
Next.js 14 srcフォルダ App router
Supabase とSupabaseのSSR認証

※Supabaseのローカル開発環境の立ち上げは経験済みを想定

Supabaseのローカル開発環境の立ち上げ手順

Local Development | Supabase Docs

Supabase CLI をインストール
Docker Desktopをインストール&立ち上げておきます。

supabase init
supabase start

↑Supabaseのローカル環境はこれだけで立ち上がります。

↓開発終了時には停止させます。

supabase stop

↑上のStopのコマンドではなくDockerのコマンドで強引に実行することも出来ます。
↓起動中のコンテナをすべて停止させます。

docker stop $(docker ps -q)

インストール

srcフォルダ付きの素のNext.jsをインストールしてその差分を見ていきます。

npx create-next-app

srcフォルダと@pathは利用します。

ファイル

package.json
{
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@supabase/ssr": "latest",
    "@supabase/supabase-js": "latest",
    "autoprefixer": "10.4.15",
    "next": "latest",
    "postcss": "8.4.29",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "tailwindcss": "3.3.3",
    "typescript": "5.1.3"
  },
  "devDependencies": {
    "@types/node": "20.3.1",
    "@types/react": "18.2.8",
    "@types/react-dom": "18.2.5"
  }
}

ライブラリのインストール

npm i @supabase/ssr @supabase/supabase-js

tailwind.config.ts
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/utils/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
export default config;

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

forceConsistentCasingInFileNamesはTypeScriptのコンパイラオプションの一つで、ファイル名の大文字と小文字の一貫性を強制します。これがtrueに設定されている場合、TypeScriptは大文字と小文字が異なるだけで異なるファイルとして扱います。これは、大文字と小文字を区別しないファイルシステム(例えば、WindowsやMacの一部)で問題を引き起こす可能性があるためです。

src\app\globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

src\app\layout.tsx
import "./globals.css";

const defaultUrl = process.env.VERCEL_URL
  ? `https://${process.env.VERCEL_URL}`
  : "http://localhost:3000";

export const metadata = {
  metadataBase: new URL(defaultUrl),
  title: "Next.js and Supabase SSR Auth",
  description: "The fastest way to build apps with Next.js and Supabase",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>
        <main className="flex flex-col items-center min-h-screen">
          {children}
        </main>
      </body>
    </html>
  );
}

src\app\page.tsx
import AuthButton from "@/components/AuthButton";

export default async function Index() {
  return (
    <div>
      <nav>
        Supabase SSR 認証でのログイン
        <AuthButton />
      </nav>
    </div>
  );
}

src\app\auth\callback\route.ts
import { createClient } from "@/utils/supabase/server";
import { NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function GET(request: Request) {
  // The `/auth/callback` route is required for the server-side auth flow implemented
  // by the Auth Helpers package. It exchanges an auth code for the user's session.
  // `/auth/callback` ルートは、Auth Helpers パッケージによって実装されたサーバーサイドの認証フローに必要です。
  // 認証コードをユーザーのセッションに交換します。
  // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange
  const requestUrl = new URL(request.url);
  const code = requestUrl.searchParams.get("code");

  if (code) {
    const cookieStore = cookies();
    const supabase = createClient(cookieStore);
    await supabase.auth.exchangeCodeForSession(code);
  }

  // URL to redirect to after sign in process completes
  // サインインプロセスが完了した後にリダイレクトする URL
  return NextResponse.redirect(requestUrl.origin);
}

src\app\login\page.tsx
import Link from 'next/link'
import { headers, cookies } from 'next/headers'
import { createClient } from '@/utils/supabase/server'
import { redirect } from 'next/navigation'

export default function Login({
  searchParams,
}: {
  searchParams: { message: string }
}) {
  const signIn = async (formData: FormData) => {
    'use server'

    const email = formData.get('email') as string
    const password = formData.get('password') as string
    const cookieStore = cookies()
    const supabase = createClient(cookieStore)

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

    if (error) {
      return redirect('/login?message=Could not authenticate user')
    }

    return redirect('/')
  }

  const signUp = async (formData: FormData) => {
    'use server'

    const origin = headers().get('origin')
    const email = formData.get('email') as string
    const password = formData.get('password') as string
    const cookieStore = cookies()
    const supabase = createClient(cookieStore)

    const { error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        emailRedirectTo: `${origin}/auth/callback`,
      },
    })

    if (error) {
      return redirect('/login?message=Could not authenticate user')
    }

    return redirect('/login?message=Check email to continue sign in process')
  }

  return (
    <div className="flex-1 flex flex-col w-full px-8 sm:max-w-md justify-center gap-2">
      <Link
        href="/"
        className="absolute left-8 top-8 py-2 px-4 rounded-md no-underline text-foreground bg-btn-background hover:bg-btn-background-hover flex items-center group text-sm"
      >
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
          className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1"
        >
          <polyline points="15 18 9 12 15 6" />
        </svg>{' '}
        Back
      </Link>

      <form
        className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground"
        action={signIn}
      >
        <label className="text-md" htmlFor="email">
          Email
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="email"
          placeholder="you@example.com"
          required
        />
        <label className="text-md" htmlFor="password">
          Password
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          type="password"
          name="password"
          placeholder="••••••••"
          required
        />
        <button className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2">
          Sign In
        </button>
        <button
          formAction={signUp}
          className="border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2"
        >
          Sign Up
        </button>
        {searchParams?.message && (
          <p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
            {searchParams.message}
          </p>
        )}
      </form>
    </div>
  )
}

src\components\AuthButton.tsx
import { createClient } from '@/utils/supabase/server'
import Link from 'next/link'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'

export default async function AuthButton() {
  const cookieStore = cookies()
  const supabase = createClient(cookieStore)

  const {
    data: { user },
  } = await supabase.auth.getUser()

  const signOut = async () => {
    'use server'

    const cookieStore = cookies()
    const supabase = createClient(cookieStore)
    await supabase.auth.signOut()
    return redirect('/login')
  }

  return user ? (
    <div className="flex items-center gap-4">
      Hey, {user.email}!
      <form action={signOut}>
        <button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
          Logout
        </button>
      </form>
    </div>
  ) : (
    <Link
      href="/login"
      className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
    >
      Login
    </Link>
  )
}

Supabaseクライアント

  • serverクライアント
  • clientクライアント
  • middlewareクライアント

※実際に認証に使うのはserverクライアントです。

src\utils\supabase\client.ts
import { createBrowserClient } from '@supabase/ssr'

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

src\utils\supabase\middleware.ts
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { type NextRequest, NextResponse } from "next/server";

export const createClient = (request: NextRequest) => {
  // Create an unmodified response
  // 変更されていないレスポンスを作成します。
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return request.cookies.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          // If the cookie is updated, update the cookies for the request and response
          // クッキーが更新された場合、リクエストとレスポンスのクッキーを更新します。
          request.cookies.set({
            name,
            value,
            ...options,
          });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({
            name,
            value,
            ...options,
          });
        },
        remove(name: string, options: CookieOptions) {
          // If the cookie is removed, update the cookies for the request and response
          // クッキーが削除された場合、リクエストとレスポンスのクッキーを更新します。
          request.cookies.set({
            name,
            value: "",
            ...options,
          });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({
            name,
            value: "",
            ...options,
          });
        },
      },
    }
  );

  return { supabase, response };
};

src\utils\supabase\server.ts
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";

export const createClient = (cookieStore: ReturnType<typeof cookies>) => {
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value, ...options });
          } catch (error) {
            // The `set` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
            // `set` メソッドはサーバーコンポーネントから呼び出されました。
            // ユーザーセッションを更新するミドルウェアがある場合は、無視できます。
          }
        },
        remove(name: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value: "", ...options });
          } catch (error) {
            // The `delete` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
            // `delete` メソッドはサーバーコンポーネントから呼び出されました。
            // ユーザーセッションを更新するミドルウェアがある場合は、無視できます。
          }
        },
      },
    }
  );
};

必要なものをインストール
npm i

ローカルサーバーを立ち上げる
npm run dev





解説

AI によるコード解説です。

解説は不要なファイル
package.json
tailwind.config.ts
tsconfig.json
src\app\globals.css
src\app\layout.tsx
src\app\page.tsx
src\utils\supabase\client.ts

src\app\auth\callback\route.ts

/auth/callback ルートは、Auth Helpers パッケージによって実装されたサーバーサイドの認証フローに必要です。
認証コードをユーザーのセッションに交換します。
https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange

  1. import { createClient } from "@/utils/supabase/server";

    • Supabaseのクライアントを作成するための関数をインポートしています。
  2. import { NextResponse } from "next/server";

    • Next.jsのサーバーサイドレンダリングに使用されるレスポンスオブジェクトをインポートしています。

レスポンスオブジェクトは、サーバーからクライアントへの応答を表現するオブジェクトです。これは通常、HTTP応答のステータスコード、ヘッダー、ボディなどの情報を含みます。

NextResponseはNext.jsの特定のレスポンスオブジェクトで、サーバーサイドレンダリングやAPIルートなどで使用されます。これを使用すると、サーバーから送信されるレスポンスの詳細をカスタマイズできます。例えば、ステータスコードを設定したり、カスタムヘッダーを追加したり、レスポンスボディを設定したりできます。

Next.jsのNextResponseは、特にミドルウェアでの使用を目的としています。ミドルウェアは、リクエストとレスポンスの間に位置するコードで、リクエストの処理方法やレスポンスの生成方法をカスタマイズできます。

  1. import { cookies } from "next/headers";

    • Next.jsからcookiesモジュールをインポートしています。これにより、クッキーを操作することができます。クッキーは、ユーザーのセッション情報などを保存するために使用されます。
  2. export async function GET(request: Request) {

    • GETリクエストを処理する非同期関数をエクスポートしています。この関数は、クライアントからのGETリクエストを受け取り、それに対するレスポンスを生成します。
  3. const requestUrl = new URL(request.url);

    • リクエストからURLを取得し、URLオブジェクトを作成しています。これにより、URLの各部分(ホスト、パス、クエリパラメータなど)に簡単にアクセスできます。
  4. const code = requestUrl.searchParams.get("code");

    • URLのクエリパラメータから "code" を取得しています。これは、認証コードを取得するために使用されます。
  5. if (code) {

    • "code" が存在する場合(つまり、クエリパラメータに "code" が含まれている場合)、次の処理を行います。
  6. const cookieStore = cookies();

    • クッキーストアを作成します。これにより、クッキーを操作することができます。
  7. const supabase = createClient(cookieStore);

    • Supabaseのクライントを作成します。これにより、データベース操作を行うことができます。
  8. await supabase.auth.exchangeCodeForSession(code);

    • 認証コードをユーザーセッションに交換します。これにより、ユーザーは認証され、セッションが開始されます。
  9. return NextResponse.redirect(requestUrl.origin);

    • ユーザーをリクエストのオリジンURLにリダイレクトします。これは、認証プロセスが完了した後にユーザーを元のページに戻すために使用されます。

このコードは、サーバーサイドで認証フローを実装するために使用されます。

 if (code) {
    const cookieStore = cookies();
    const supabase = createClient(cookieStore);
    await supabase.auth.exchangeCodeForSession(code);
  }

このコードは、認証コードを使用してセッションを取得し、そのセッション情報をクッキーストアに保存しています。そして、そのクッキーストアはブラウザのクッキーとして保存されます。

具体的には、以下のような流れになります:

  1. cookies()関数を呼び出してクッキーストアを作成します。このクッキーストアは、ブラウザのクッキーを管理するためのオブジェクトです。

  2. createClient(cookieStore)を呼び出してSupabaseクライアントを作成します。このクライアントは、クッキーストアを使用してユーザーの認証情報を管理します。

  3. supabase.auth.exchangeCodeForSession(code)を呼び出して、認証コードを使用してセッションを取得します。このセッションは、ユーザーの認証情報を含むオブジェクトです。

  4. 取得したセッション情報は、Supabaseクライアントによってクッキーストアに保存されます。そして、そのクッキーストアはブラウザのクッキーとして保存されます。

これにより、ユーザーが次回サイトを訪れたときに、ブラウザのクッキーからセッション情報を読み取り、ユーザーを認証することができます。

src\components\AuthButton.tsx

  1. import { createClient } from '@/utils/supabase/server'

    • これは、Supabaseクライアントを作成するための関数をインポートしています。
  2. import Link from 'next/link'

    • これは、Next.jsのLinkコンポーネントをインポートしています。
  3. import { cookies } from 'next/headers'

    • これは、Next.jsのheadersモジュールからcookies関数をインポートしています。
  4. import { redirect } from 'next/navigation'

    • これは、Next.jsのnavigationモジュールからredirect関数をインポートしています。redirect関数は、ユーザーを別のページにリダイレクトするために使用されます。
  5. export default async function AuthButton() {

    • これは、非同期のAuthButton関数をエクスポートしています。この関数は、認証ボタンの動作を定義します。
  6. const cookieStore = cookies()

    • これは、cookies関数を呼び出してクッキーストアを作成し、それをcookieStore変数に保存しています。クッキーストアは、クッキーの操作を行うために使用されます。
  7. const supabase = createClient(cookieStore)

    • これは、createClient関数を呼び出してSupabaseクライアントを作成し、それをsupabase変数に保存しています。Supabaseクライアントは、Supabase APIとの通信を行うために使用されます。
  8. const { data: { user }, } = await supabase.auth.getUser()

    • これは、SupabaseクライアントのauthオブジェクトのgetUserメソッドを呼び出して、現在のユーザー情報を取得しています。この情報は、ユーザーがログインしているかどうかを判断するために使用されます。
  9. const signOut = async () => { 'use server'

    • これは、非同期のsignOut関数を定義しています。この関数は、ユーザーがログアウトするときの動作を定義します。
  10. const cookieStore = cookies()

    • これは、cookies関数を呼び出してクッキーストアを作成し、それをcookieStore変数に保存しています。クッキーストアは、クッキーの操作を行うために使用されます。
  11. const supabase = createClient(cookieStore)

    • これは、createClient関数を呼び出してSupabaseクライアントを作成し、それをsupabase変数に保存しています。Supabaseクライアントは、Supabase APIとの通信を行うために使用されます。
  12. await supabase.auth.signOut()

    • これは、SupabaseクライアントのauthオブジェクトのsignOutメソッドを呼び出して、ユーザーをログアウトさせています。
  13. return redirect('/login')

    • これは、ユーザーをログインページにリダイレクトしています。これは、ユーザーがログアウトした後にログインページに戻るために使用されます。
  14. return user ? (

    • これは、ユーザーがログインしているかどうかをチェックしています。ログインしている場合は、ユーザーのメールアドレスとログアウトボタンを表示します。ログインしていない場合は、ログインリンクを表示します。
  15. <div className="flex items-center gap-4">

    • この行は、ログインしているユーザーの情報とログアウトボタンを含むdiv要素を作成しています。CSSクラスを使用して、要素のスタイルを定義しています。
  16. Hey, {user.email}!

    • この行は、ログインしているユーザーのメールアドレスを表示しています。{user.email}は、ユーザーのメールアドレスを含むJavaScript式です。
  17. <form action={signOut}>

    • この行は、ログアウトボタンを含むform要素を作成しています。action属性には、ログアウトボタンがクリックされたときに実行される関数(この場合はsignOut関数)が設定されています。
  18. <button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">

    • この行は、ログアウトボタンを作成しています。CSSクラスを使用して、ボタンのスタイルを定義しています。
  19. Logout

    • この行は、ログアウトボタンのラベルを設定しています。
  20. </form>

    • この行は、form要素を閉じています。
  21. </div>

    • この行は、div要素を閉じています。
  22. ) : (

    • この行は、userが存在しない場合(つまりユーザーがログインしていない場合)に実行されるコードブロックを開始しています。
  23. <Link href="/login" className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">

    • この行は、ログインリンクを作成しています。LinkコンポーネントはNext.jsのルーティング機能を利用して、指定したパス(この場合は"/login")へのリンクを作成します。リンクのスタイルは、classNameプロパティを通じてCSSクラスで定義されます。
  24. <a>Login</a>

    • この行は、ログインリンクのラベルを設定しています。
  25. </Link>

    • この行は、Linkコンポーネントを閉じています。
  26. )

    • この行は、userが存在しない場合に実行されるコードブロックを閉じています。
  27. }

    • この行は、AuthButton関数を閉じています。

このコードブロックは、AuthButton関数の最後の部分で、ユーザーがログインしていない場合に実行されます。ユーザーがログインしているかどうかは、user変数の値をチェックすることで判断されます。userが存在する場合(つまりユーザーがログインしている場合)、ログアウトボタンとユーザーのメールアドレスが表示されます。userが存在しない場合(つまりユーザーがログインしていない場合)、ログインリンクが表示されます。

src\app\login\page.tsx

  1. import Link from 'next/link'

    • Next.jsのLinkコンポーネントをインポートしています。これは、アプリケーション内の異なるページ間でのナビゲーションを可能にします。
  2. import { headers, cookies } from 'next/headers'

    • Next.jsのheaderscookiesユーティリティをインポートしています。これらは、HTTPヘッダーやクッキーの操作を容易にします。
  3. import { createClient } from '@/utils/supabase/server'

    • Supabaseクライアントを作成するためのユーティリティ関数をインポートしています。
  4. import { redirect } from 'next/navigation'

    • Next.jsのredirectユーティリティをインポートしています。これは、ユーザーを別のページにリダイレクトするために使用します。
  5. export default function Login({ searchParams, }: { searchParams: { message: string } })

    • Loginという名前のReactコンポーネントをエクスポートしています。このコンポーネントは、ログインフォームを表示し、ユーザーがログインできるようにします。
  6. const signIn = async (formData: FormData) => { 'use server'

    • signInという名前の非同期関数を定義しています。この関数は、フォームデータを受け取り、そのデータを使用してユーザーをサインインします。
  7. const email = formData.get('email') as string

    • フォームデータからemailフィールドの値を取得しています。この値は、ユーザーが入力したメールアドレスです。
  8. const password = formData.get('password') as string

    • フォームデータからpasswordフィールドの値を取得しています。この値は、ユーザーが入力したパスワードです。
  9. const cookieStore = cookies()

    • cookies関数を使用して、クッキーストアを作成しています。クッキーストアは、クッキーの操作を容易にします。
  10. const supabase = createClient(cookieStore)

    • createClient関数を使用して、Supabaseクライアントを作成しています。このクライアントは、SupabaseのAPIを使用するために必要です。
  11. const { error } = await supabase.auth.signInWithPassword({ email, password, })

    • SupabaseクライアントのsignInWithPasswordメソッドを使用して、ユーザーをサインインしています。このメソッドは、メールアドレスとパスワードを使用してユーザーを認証します。
  12. if (error) { return redirect('/login?message=Could not authenticate user') }

    • エラーが発生した場合(つまり、ユーザーの認証に失敗した場合)、ユーザーをログインページにリダイレクトします。リダイレクトURLにはエラーメッセージが含まれています。
  13. return redirect('/')

    • ユーザーの認証が成功した場合、ユーザーをホームページにリダイレクトします。
  14. const signUp = async (formData: FormData) => { 'use server'

    • signUpという名前の非同期関数を定義しています。この関数は、フォームデータを受け取り、そのデータを使用して新しいユーザーを登録します。

Action Server

  1. const origin = headers().get('origin')

    • headers関数を使用して、HTTPヘッダーからoriginを取得しています。originは、リクエストが発生したオリジン(つまり、プロトコル、ドメイン、およびポート)を示します。
  2. const email = formData.get('email') as string

    • フォームデータからemailフィールドの値を取得しています。この値は、ユーザーが入力した
  3. const password = formData.get('password') as string

    • フォームデータからpasswordフィールドの値を取得しています。この値は、ユーザーが入力したパスワードです。
  4. const cookieStore = cookies()

    • cookies関数を使用して、クッキーストアを作成しています。クッキーストアは、クッキーの操作を容易にします。
  5. const supabase = createClient(cookieStore)

    • createClient関数を使用して、Supabaseクライアントを作成しています。このクライアントは、SupabaseのAPIを使用するために必要です。
  6. const { error } = await supabase.auth.signUp({ email, password, options: { redirectTo: origin, }, })

    • SupabaseクライアントのsignUpメソッドを使用して、新しいユーザーを登録しています。
      このメソッドは、メールアドレスとパスワードを使用して新しいユーザーを作成します。ユーザーの作成が成功すると、ユーザーはoriginにリダイレクトされます。
  7. if (error) { return redirect('/login?message=Could not register user') }

    • エラーが発生した場合(つまり、ユーザーの登録に失敗した場合)、ユーザーをログインページにリダイレクトします。リダイレクトURLにはエラーメッセージが含まれています。
  8. return redirect('/')

    • ユーザーの登録が成功した場合、ユーザーをホームページにリダイレクトします。

このコードブロックは、signUp関数の最後の部分で、新しいユーザーの登録を行います。
ユーザーの登録は、メールアドレスとパスワードを使用して行います。登録に成功した場合、ユーザーはホームページにリダイレクトされます。
登録に失敗した場合、エラーメッセージとともにログインページにリダイレクトされます。

Supabaseクライアント

src\utils\supabase\middleware.ts

このコードは、Next.jsのサーバーサイドでSupabaseクライアントを作成するための関数createClientをエクスポートしています。

  1. import { createServerClient, type CookieOptions } from "@supabase/ssr";

    • Supabaseのサーバーサイドレンダリング(SSR)用のライブラリからcreateServerClient関数とCookieOptions型をインポートしています。createServerClientは、サーバーサイドでSupabaseクライアントを作成するための関数です。CookieOptionsは、クッキーの設定オプションを表す型です。
  2. import { type NextRequest, NextResponse } from "next/server";

    • Next.jsのサーバーサイドコンポーネントからNextRequestNextResponseをインポートしています。これらは、HTTPリクエストとレスポンスを表す型です。
  3. export const createClient = (request: NextRequest) => {

    • createClientという関数をエクスポートしています。この関数は、HTTPリクエストを引数に取り、Supabaseクライアントを作成します。
  4. let response = NextResponse.next({

    • NextResponse.nextメソッドを使用して、新しいレスポンスオブジェクトを作成しています。このレスポンスオブジェクトは、後続の処理で使用されます。
  5. request: { headers: request.headers, }, });

    • レスポンスオブジェクトに、リクエストヘッダーを含むリクエストオブジェクトを設定しています。これにより、レスポンスオブジェクトは、リクエストの詳細を知ることができます。
  6. const supabase = createServerClient(

    • createServerClient関数を使用して、Supabaseクライアントを作成しています。このクライアントは、Supabaseの各種機能を使用するためのインターフェースを提供します。
  7. process.env.NEXT_PUBLIC_SUPABASE_URL!,

    • 環境変数NEXT_PUBLIC_SUPABASE_URLからSupabaseサービスのURLを取得しています。このURLは、Supabaseクライアントが接続するサーバーを指定します。
  8. process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,

    • 環境変数NEXT_PUBLIC_SUPABASE_ANON_KEYからSupabaseサービスの匿名キーを取得しています。このキーは、Supabaseクライアントがサーバーと通信するための認証情報を提供します。
  9. get(name: string): このメソッドは、指定した名前のクッキーの値を取得します。request.cookies.get(name)?.valueというコードで、リクエストからクッキーを取得しています。?.はオプショナルチェイニング演算子と呼ばれ、request.cookies.get(name)undefinedまたはnullを返した場合にエラーを防ぎます。

  10. set(name: string, value: string, options: CookieOptions): このメソッドは、指定した名前、値、オプションで新しいクッキーを設定します。まず、request.cookies.setでリクエストのクッキーを設定し、次にresponse.cookies.setでレスポンスのクッキーを設定します。これにより、クッキーの変更がリクエストとレスポンスの両方に反映されます。optionsはオプショナルで、クッキーの有効期限やパスなどを指定できます。

  11. remove(name: string, options: CookieOptions): このメソッドは、指定した名前のクッキーを削除します。クッキーの削除は、そのクッキーの値を空文字列に設定することで行います。setメソッドと同様に、リクエストとレスポンスの両方のクッキーを更新します。

src\utils\supabase\server.ts

  1. import { createServerClient } from "@supabase/ssr";

    • Supabaseのサーバーサイドレンダリング(SSR)ライブラリからcreateServerClient関数をインポートしています。この関数は、サーバーサイドでSupabaseクライアントを作成するために使用されます。サーバーサイドでデータベース操作を行うことができます。
  2. import type { CookieOptions } from "@supabase/ssr";

    • SupabaseのSSRライブラリからCookieOptions型をインポートしています。この型は、クッキーのオプションを定義するために使用されます。
      型安全性を提供し、クッキーのオプションを正しく設定することを保証する点です。
  3. import { cookies } from "next/headers";

    • Next.jsのheadersモジュールからcookies関数をインポートしています。この関数は、クッキーストアを作成するために使用されます。これはクッキーの管理を容易します。
  4. export const createClient = (cookieStore: ReturnType<typeof cookies>) => {

    • createClientという名前の関数をエクスポートしています。この関数は、引数としてクッキーストアを受け取り、Supabaseクライアントを作成します。
      クッキーストアを使用して認証情報を管理するSupabaseクライアントを簡単に作成します。
  5. return createServerClient(

    • createServerClient関数を呼び出し、その結果を返しています。この関数は、Supabaseクライアントを作成するために使用されます。
  6. process.env.NEXT_PUBLIC_SUPABASE_URL!,

    • 環境変数からSupabaseのURLを取得しています。このURLは、Supabaseクライアントを作成するために必要です。
  7. process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,

    • 環境変数からSupabaseの匿名キーを取得しています。このキーは、Supabaseクライアントを作成するために必要です。
  8. { cookies: {

    • クッキーのオプションを設定しています。このオプションは、Supabaseクライアントがクッキーをどのように扱うかを定義します。
  9. get(name: string) { return cookieStore.get(name)?.value; },

    • クッキーのgetメソッドを定義しています。このメソッドは、指定した名前のクッキーの値を取得するために使用されます。クッキーの値を取得することで、ユーザーの状態や設定を維持することができます。
  10. set(name: string, value: string, options: CookieOptions) { try { cookieStore.set({ name, value, ...options }); } catch (error) { /* error handling */ } },

    • クッキーのsetメソッドを定義しています。このメソッドは、指定した名前と値で新しいクッキーを設定するために使用されます。クッキーを設定することで、ユーザーの状態や設定を保存することができます。
  11. remove(name: string, options: CookieOptions) { try { cookieStore.set({ name, value: "", ...options }); } catch (error) { /* error handling */ } }

    • クッキーのremoveメソッドを定義しています。このメソッドは、指定した名前のクッキーを削除するために使用されます。クッキーを削除することで、不要なデータをクリアし、ユーザーのプライバシーを保護することができます。
9
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
9
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?