Next.jsでsupabaseとprismaを使ってログイン機能作ってみた

# はじめに
この記事では、supabaseのauthenticationを使ったログイン機能とprismaの使い方の個人的な備忘録です
supabaseでログイン機能はできたけど、どうやってリレーションするとか認証機能作るとかわからない人にオススメです
また、セキュリティ面なども考慮したつもりですが、怪しいところ等ありましたらコメントで教えていただけると幸いです！

# 完成イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/80f61914-79f6-6e5e-e552-9a55abf74c90.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/30ab8cbd-f852-d48e-6c94-90cc14bbaa8a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/e71e6e14-412d-d142-8ca4-d563b99e019a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/776bf997-0ee8-706f-750a-4fc734517bbf.png)
ログイン中のユーザー表示やログイン中のユーザーの投稿のみを表示したりします！

コードはこちら

https://github.com/naoya25/supabase-prisma-login-app/

# supabaseとは

SupabaseはFirebaseに代わるオープンソースです。
Postgresデータベース、認証、インスタントAPI、エッジファンクション、リアルタイムサブスクリプション、ストレージ、Vectorエンベッディングでプロジェクトを始めましょう。

# prismaとは

Prisma は、チームがデータベースを操作し操作するための最高のエクスペリエンスを提供します。接続プーリング、キャッシュ、リアルタイム データベース サブスクリプションなどの複雑な作業も、当社の製品を使用すると簡単に行えます。 アプリケーションを構築し、すべてがスムーズに実行されるように最適化し、ユーザーと要件に合わせて成長します。

# supabaseアカウント作成・プロジェクト作成
https://supabase.com/

[公式ページ](https://supabase.com/)のStart your projectからアカウントを作成します
![Screenshot 2024-03-06 13.52.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/9800696f-aadf-bd4d-7238-54c666c1c892.png)

アカウントが作成できたら[Dashboard](https://supabase.com/dashboard/projects)からNew projectを作成します
![Screenshot 2024-03-06 13.56.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/70839cb5-b634-0f7b-790a-6406845b709c.png)

NameとDatabase Passwordは自分で適当に決めておっけです
あとでDatabase Passwordは使うのでメモしておきましょう！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/1ae8d1e5-601a-4e0a-d750-1f87be0a6562.png)

Create new projectをクリックすると新しいプロジェクトを作り始めます
この画面ではちょっと待機します（自分の場合は1分程度で作成完了しました）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/9c355f7f-88ba-35b3-1521-64a5d75d81bb.png)

作成が完了するとHome画面になってるかと思います
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/995b2615-ea1f-00f8-e3a1-d5c15ced0320.png)

一応今回使う左のメニュー説明しときます
`Table Editor`→このあと作成するprismaで作ったデータベース表示
`SQL Editor`→ログイン処理後のprismaとのやりとりを記述（supabaseのpostgreSQLを直接操作できます）
`Authenitication`→ログイン機能
`Storage`→ユーザー画像を登録
`Logs`→エラーが出た時にみる
`Project Settings`→Nextプロジェクトとsupabaseを繋ぐURLとか見ます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/632c0c48-a7a3-91a2-0781-e7680bde05c4.png)


終わり

# Next.jsプロジェクト作成

ここからはNext.jsのプロジェクトを作成していじっていきます
```bash
npx create-next-app@latest
```
バージョンは、`14.1.2`です
詳細はこんな感じで作りました
（自分の好きなように作って良いかと思います）
```bash
✔ What is your project named? … qiita-test-app
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
✔ What import alias would you like configured? … @/*
```

# Prismaの設定

ディレクトリを移動して必要なライブラリをインストールします

```bash
cd qiita-test-app/
npm i prisma --save-dev
npm i @prisma/client
npx prisma init
```
新しくschema.prismaというファイルが作成されているかと思います
データベースに新しいテーブルを作成したいときは、schema.prismaを変更します

今回はユーザー情報を保持するテーブルとブログ投稿用のPostテーブルを作成し、リレーションしておきます
```prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider          = "postgresql"
  url               = env("DATABASE_URL")
}

model User {
  id    Int     @id @default(autoincrement())
  auth_id String @unique
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String
  content   String?
  published Boolean  @default(false)
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}
```

次にsupabaseと繋ぐためのURLを持ってきます
`.env`ファイルを作成します

```.env
DATABASE_URL="*************"
```
`DATABASE_URL`は、project settingsのDatabaseから取得できます
![Screenshot 2024-03-06 18.37.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/fc32ac5c-325a-269d-ab7b-19ce87ca7771.png)

こんな感じになってると思います(多少違うかもしれません)
`postgres://postgres.aiolelfrdszjtreachau:[YOUR-PASSWORD]@aws-0-ap-northeast-1.pooler.supabase.com:5432/postgres`
[YOUR-PASSWORD]はsupabaseのプロジェクトを作成したときのパスワードに置き換えてください
パスワードが123456なら次のようにしてください
`postgres://postgres.aiolelfrdszjtreachau:123456@aws-0-ap-northeast-1.pooler.supabase.com:5432/postgres`
envファイルを作成できたらmigrateしてsupabaseのデータベースにテーブルを追加します
```bash
npx prisma migrate dev --name init
```
うまく作成が完了するとsupabaseのDatabaseからUserとPostと_prisma_migrationsというテーブルが作成できていることが確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/f095e5da-62b5-7074-60c2-1d11b0a6a707.png)

# supabaseの設定
ここまでで、prismaを介してsupabaseのデータベースを利用できるようになっています
しかし、supabase側でAutheniticationからユーザー登録してもTable Editorからは確認できません
どういうことかというと、supabaseの機能を使ってアカウントを作ってもその情報がprismaで作ったデータベースに保存されないよってことです

まず、AutheniticationのProvidersからConfirm emailをオフにしておきます
![Screenshot 2024-03-06 22.45.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/d6eb0c66-7366-7514-ae85-1b785648e2af.png)

次にAutheniticationによるsign upがされたときに、Userにデータを追加するように設定します
SQL Editorで次のコードを実行します
```SQL Editor
create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = public
as $$
begin
  insert into public."User" (auth_id, email)
  values (new.id, new.email);
  return new;
end;
$$;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();
```
Runを押して、`Success. No rows returned`と出力されればおっけです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/b296f8e6-db24-2f78-58b7-0e5540fda086.png)

これでsign upされたらuserが追加されるようになるはずです
Autheniticationでsign upしてみてエラーが出るようでしたら、LogsのAuthからエラーを確認してみてください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/319dd92e-7add-c421-7b70-20a70397a20e.png)

ここまでsupabase, prismaはほぼほぼ終了です！あとはNext.jsいじっていきます〜〜

# Nextプロジェクトの設定
とりあえず必要なライブラリをインストールしておきます
```bash
npm i @supabase/supabase-js
```
envに必要なコードを追加します
```diff:.env
+ NEXT_PUBLIC_SUPABASE_URL="***************"
+ NEXT_PUBLIC_SUPABASE_API_KEY="***************"
```
これらはsupabaseのProject Settings→APIから確認できます
![Screenshot 2024-03-10 5.23.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/8b9d0074-14e0-04b4-29b1-b9e1c52b9c11.png)

supabase clientを作成します
これはenvに書いたコードを使ってnext側からsupabaseにアクセスするためのものです
```typescript:utils/supabase.ts
import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL as string,
  process.env.NEXT_PUBLIC_SUPABASE_API_KEY as string
);
```
ログインフォームを作る前にまずは認証関連を扱うカスタムフックスを作成します

```ts:src/hooks/useUser.ts
"use client";
import { supabase } from "@/utils/supabase";
import { useEffect, useState } from "react";
import { Session } from "@supabase/supabase-js";
import { UserType } from "@/types/user";

export default function useUser() {
  const [session, setSession] = useState<Session | null>(null);
  const [user, setUser] = useState<UserType | null>(null);

  useEffect(() => {
    const { data: authListener } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setSession(session);
      }
    );

    return () => {
      authListener.subscription.unsubscribe();
    };
  }, []);

  useEffect(() => {
    const setupUser = async () => {
      if (session?.user.id) {
        const response = await fetch(`/api/user/${session.user.id}`);
        if (response.ok) {
          const data = await response.json();
          setUser(data.user);
        } else {
          console.error("Failed to fetch user data");
        }
      }
    };
    setupUser();
  }, [session]);

  function signUp({ email, password }: { email: string; password: string }) {
    supabase.auth.signUp({ email, password });
  }

  function signIn({ email, password }: { email: string; password: string }) {
    supabase.auth.signInWithPassword({ email, password });
  }

  function signOut() {
    supabase.auth.signOut();
  }

  return { session, user, signUp, signIn, signOut };
}
```
このカスタムフックスで、ユーザーのセッション、情報、サインアップ処理、サインイン処理、サインアウト処理を一括で行います

サインアップ処理に関しては、supabaseのAuthenticationで行っており先ほど作った`utils/supabase.ts`に設定が書かれています
その後、サインアップしたユーザーの情報をprismaを通じて取得します
これは`src/app/api/user/[auth_id]/route.ts`に設定したAPIを使ってsupabaseのAuthenticationでサインアップしたユーザーidをprismaで作成したUserテーブルのauth_idと検索してデータを取得しています

```ts:src/app/api/user/[auth_id]/route.ts
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";

export const prisma = new PrismaClient() as any;
export async function doConnect() {
  try {
    await prisma.$connect();
  } catch (error) {
    return Error("DB接続に失敗しました");
  }
}
export const GET = async (req: Request, res: NextResponse) => {
  const auth_id: string = req.url.split("/user/")[1];
  console.log(auth_id);

  try {
    await doConnect();
    const user = await prisma.user.findUnique({
      where: { auth_id },
      include: {
        posts: true,
      },
    });
    return NextResponse.json({ message: "Success", user }, { status: 200 });
  } catch (error) {
    return NextResponse.json({ message: "Error", error }, { status: 500 });
  } finally {
    await prisma.$disconnect();
  }
};
```

このカスタムフックスさえ作成できてしまえばあとはフォームを作るだけです
フォームにはreact-hook-formを使っています
```bash
npm i react-hook-form
```
コードは長くなるのでgithubを見てみてください
- [ログインフォーム](https://github.com/naoya25/supabase-prisma-login-app/blob/main/src/app/user/login/page.tsx)
- [サインアップフォーム](https://github.com/naoya25/supabase-prisma-login-app/blob/main/src/app/user/register/page.tsx)

ログイン中のユーザーの情報等は、カスタムフックスから全て取得できるので、自由にカスタマイズしてみてください

```ts
const { session, user, signUp, signIn, signOut } = useUser();
```
基本的には、`session`が`null`の時は未ログインで、ログイン中の時は`user`にユーザー情報が入ってるだけです

これを使って投稿機能も作成していますが、記事が長くなったし他の記事と対して変わらないので説明は飛ばします〜〜
UserとPostがリレーションされているので、
投稿する時にはauthoerIdを一緒に投稿することと、
投稿を表示するときは、ログイン中のuser情報の中にその人のposts情報が入っていること
に注意してください

# 終わり
終わりです
あざした〜〜