Skip to content

shinaps/nextjs-approuter-supabase-todoapp

Repository files navigation

Next.jsのAppRouterでSupabaseの認証機能を実装する方法

環境構築

リポジトリの作成

ハリボテTODOアプリをテンプレートとしてリポジトリを作成します。

https://github.com/shinaps/nextjs-haribote-todo-app

%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-01-27_17 22 51

できました。

https://github.com/shinaps/nextjs-approuter-supabase-todoapp

作成したリポジトリをクローン

git clone git@github.com:shinaps/nextjs-approuter-supabase-todoapp.git
Cloning into 'nextjs-approuter-supabase-todoapp'...
Warning: Permanently added 'github.com' (ED25519) to the list of known hosts.
remote: Enumerating objects: 44, done.
remote: Counting objects: 100% (44/44), done.
remote: Compressing objects: 100% (39/39), done.
remote: Total 44 (delta 0), reused 42 (delta 0), pack-reused 0
Receiving objects: 100% (44/44), 71.36 KiB | 445.00 KiB/s, done.

ハリボテTODOアプリ起動

npm install
npm run dev
%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-01-27_17 32 34

Supabaseでプロジェクトを作成

https://supabase.com/dashboard/new

%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-01-27_18 20 03

実装

以下のページを参考に実装していきます。

https://supabase.com/docs/guides/auth/server-side/nextjs

パッケージのインストール

npm install @supabase/ssr

環境変数の設定

https://supabase.com/dashboard/project/{Reference ID}/settings/api のURLにアクセスしてProject URLとProject API keysのanon key を.env.localに転記します。

# .env.local
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key

以下の手順は今回はスキップ可能です。スキップする場合はcreateBrowserClient<Database>

createBrowserClientのように書き換えて使用してください。

Supabaseの型情報をクライアントで使用できるようにする(オプショナル)

Supabaseの型情報を生成

https://supabase.com/docs/reference/cli/supabase-gen-types-typescript

https://supabase.com/docs/reference/javascript/typescript-support#generating-typescript-types

npm install supabase
npx supabase login
supabase init
supabase link --project-ref {Reference ID}
supabase gen types typescript --linked > supabase/database.types.ts

これでsupabase/database.types.ts に型の情報が生成されます。

supabase link --project-ref {Reference ID} の時にDBのパスワードが求められるので、忘れてしまった場合は以下のボタンをクリックすればリセットできます。

https://supabase.com/dashboard/project/{Reference ID}/settings/databaseのURLからページにアクセスできます。

%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-01-28_0 35 37

clientに型情報を追加

以下のようにジェネリック型にDatabaseという型を渡すことで、データベースの型を参照できるようになります。

import { createBrowserClient } from "@supabase/ssr";
import { Database } from "../../../supabase/database.types";

export const createClient = () => {
  return createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  );
};

クライアントの作成

今回は1種類(Server ActionsとRoute Handlersで使用するクライアント)しか作成する必要はありませんが、実際にアプリケーションで使用する場合は、3種類作成することになると思います。その3種類というのは以下の3つです。

  • Client Componentsで使用するクライアント
  • Server Componentsで使用するクライアント
  • Server ActionsとRoute Handlersで使用するクライアント

サーバーサイドで使用するクライアントが2種類あるのは以下のように、Server ComponentsではCookieの書き込みができないという特徴があるからです。

Cookieの読み取り Cookieの書き込み
Server Components できる できない
Route Handlers できる できる
Server Actions できる できる

Server ActionsとRoute Handlersで使用するクライアントにはcookie-writable-server-client.ts という名前をつけましたが、他にいい名前があればそれを使用してください。

import { type CookieOptions, createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { Database } from "../../../supabase/database.types";
export const createClient = (cookieStore: ReturnType<typeof cookies>) => {
return createServerClient<Database>(
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) {
cookieStore.set({ name, value, ...options });
},
remove(name: string, options: CookieOptions) {
cookieStore.set({ name, value: "", ...options });
},
},
},
);
};

ミドルウェアの作成

ミドルウェアでは Cookieの情報を使用してuserの情報を取得し、userが取得できない場合はサインインのページにリダイレクトする処理になっています。

プロバイダーの設定(後で行います)でConfirm emailONになっている場合、メールアドレスの確認が完了していないユーザーはサインアップできていてもNULLになるので気をつけてください。

import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
import { Database } from "../supabase/database.types";
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient<Database>(
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) {
request.cookies.set({
name,
value,
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value,
...options,
});
},
remove(name: string, options: CookieOptions) {
request.cookies.set({
name,
value: "",
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value: "",
...options,
});
},
},
},
);
const {
data: { user },
} = await supabase.auth.getUser();
const signInPath = request.nextUrl.pathname === "/sign-in";
if (!user && !signInPath) {
const baseUrl = request.nextUrl.origin;
return NextResponse.redirect(`${baseUrl}/sign-in`);
}
return response;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

プロバイダーの設定

今回はメールアドレスの検証の実装は行わないので、

https://supabase.com/dashboard/project/{Reference ID}/auth/providers のURLにアクセスし、Auth Providers > Email > Confirm emailをOFFにします。

サインイン用のactionを作成

自分はわかりやすいのでsrc/app/services/auth/sign-in.action.ts という名前にして関数名にもActionをつけてますが、actionを付けないといけないという決まりはありません。

"use server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { createClient } from "@/clients/supabase/cookie-writable-server-client";
export const signinAction = async (formData: FormData) => {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const data = {
email: formData.get("email") as string,
password: formData.get("password") as string,
};
const { error } = await supabase.auth.signInWithPassword(data);
if (error) {
console.error(error);
redirect("/error");
}
redirect("/");
};

サインアップ用のactionを作成

サインイン用のactionとほぼ同じです。

"use server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { createClient } from "@/clients/supabase/cookie-writable-server-client";
export const signupAction = async (formData: FormData) => {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const data = {
email: formData.get("email") as string,
password: formData.get("password") as string,
};
const { error } = await supabase.auth.signUp(data);
if (error) {
redirect("/error");
}
redirect("/");
};

サインイン用のページを作成

プログレッシブエンハンスメントは捨てました。use clientを使います。

"use client";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { signinAction } from "@/app/services/auth/sign-in.action";
import { signupAction } from "@/app/services/auth/sign-up.action";
const formSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
export default function SignInOrSignUp() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
password: "",
},
});
return (
<div className="max-w-sm mx-auto mt-32">
<Form {...form}>
<form className="space-y-12">
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>email</FormLabel>
<FormControl>
<Input placeholder="Enter your em@1l" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter p@ssw0rd"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="space-y-4">
<Button formAction={signinAction} type="submit" className="w-full">
Sign In
</Button>
<Button formAction={signupAction} type="submit" className="w-full">
Sign Up
</Button>
</div>
</form>
</Form>
</div>
);
}

ここは以下のページを見ていただければわかると思うので、説明は省きます。

わからなかったらコメントかDMしてください。

Data Fetching: Server Actions and Mutations

About

Next.jsのApp RouterとSupabaseを使用したサンプルアプリ

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published