Next.js ハンズオン-その2: Supabaseで認証機能と掲示板機能を実装する
はじめに
Supabaseを用いてユーザーの新規登録・ログインなどの認証機能を実装し、ユーザーが掲示板にコメントを投稿できるようにする。
Step1. Supabaseの登録
Supabaseのアカウントがなければ、まずは https://supabase.com/ から登録する。
ログイン後、[New Project]から以下のように必要な情報を埋める。
プロジェクト作成後、以下のようにAPIキーを取得できる。
Step2. Supabaseにおいてデータベースの設計をする
今回のハンズオンで使用するデータベーステーブルの構造は、ユーザー認証と投稿機能をベースに以下のように設計することが考えられる。
どんなテーブルを作るべきか?
・usersテーブル
id: uuid - ユーザーのユニークID
email: varchar - ユーザーのメールアドレス(認証に使用)
username: varchar - ユーザーの名前
created_at: timestamp - アカウント作成日
updated_at: timestamp - アカウント更新日
・postsテーブル
id: int8 - 投稿のユニークID
user_id: uuid - 投稿者のユーザーID(usersテーブルと外部キーで関連)
title: varchar - 投稿のタイトル
content: text - 投稿の内容
created_at: timestamp - 投稿作成日
updated_at: timestamp - 投稿更新日
・commentsテーブル
id: int8 - コメントのユニークID
post_id: int8 - コメントが紐づいている投稿のID(postsテーブルと外部キーで関連)
user_id: uuid - コメントを投稿したユーザーID(usersテーブルと外部キーで関連)
content: text - コメント内容
created_at: timestamp - コメント作成日
updated_at: timestamp - コメント更新日
リレーション構造は?
・usersテーブルとpostsテーブルは1対多の関係である。つまり、1人のユーザーが複数の投稿を作成できる。
・postsテーブルとcommentsテーブルも1対多の関係である。1つの投稿に複数のコメントが付けられる形になる。
考えたDBをSupabase上に構築しよう
画面一番左のTable Editorからテーブルの編集ができる。
以下のようにして上記で考えたDBを構築する。
usersテーブルができたら[+New Table]からpostsなども追加しよう。
postsを追加する際に、postsテーブルのuser_idとusersテーブルのidを紐づける必要がある。Foreign keyというところ追加できる
commentsテーブルはこんな感じです
開発段階でメール二段階認証はめんどくさい
Authenticationの設定からオフにできます
補足: Row level securityの設定(必須)
DBを安全に取り扱うために必要。postをするにも、getをするにもこの設定をしていないとエラーを出す。例えばpostsテーブルを選択した状態で[Add RLS Policy]ボタンをクリック
以下の画面でポリシーを設定していく。[Create Policy]
認証されたユーザーのみが投稿を挿入できるようにするポリシーの例を示す。
ポリシー名: Allow authenticated insert
Action: INSERT
Target roles: authenticated
Policy definition:
(auth.uid() = user_id)
このポリシーでは、auth.uid()(現在の認証されたユーザーのID)がuser_idフィールドと一致する場合に挿入を許可する。
postsテーブルの内容を表示するときにはSELECTをする。全ユーザーがselectをできるようにrow level securityを以下のように設定する。
signupの際にuserも登録するので以下のようにusersテーブルにもrow level securityを設定しておく。
Step3. Next.jsプロジェクトを作る
$ npx create-next-app@latest supabase-handson --typescript
`$ npm install @supabase/supabase-js
プロジェクトディレクトリ構造
my-app/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── dashboard/
│ │ └── page.tsx
├── lib/
│ └── supabase.ts
├── components/
│ └── Auth.tsx
├── types
| └──supabaseTypes.ts
└── .env.local
基本ページ
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
import Link from "next/link";
export default function Home() {
return (
<div>
<h1>Welcome to My App</h1>
<Link href="/dashboard">Go to Dashboard</Link>
</div>
);
}
Step4. 認証機能の実装
SupabseでプロジェクトのHomeにあるProject APIの中に以下二つが書いてある。
.env.localに以下を記述する。
NEXT_PUBLIC_SUPABASE_URL=supabaseのProject URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=supabaseのAPI Key(anonというタグが付いているもの)
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
'use client';
import { useState } from "react";
import { supabase } from "../lib/supabase";
export default function Auth() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [message, setMessage] = useState("");
const signIn = async () => {
const { user, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
setMessage(error.message);
} else {
setMessage("Signed in successfully!");
}
};
const signUp = async () => {
const { data, error } = await supabase.auth.signUp({
email,
password,
});
if (error) {
setMessage(error.message);
} else {
setMessage("Signed up successfully! Please check your email for confirmation.");
// 登録成功後、ユーザー情報を`users`テーブルに挿入
if (data?.user) {
const { error: dbError } = await supabase.from("users").insert([
{ id: data.user.id, email: data.user.email }
]);
if (dbError) {
console.error("Error inserting user into database:", dbError.message);
} else {
console.log("User inserted into database successfully!");
}
}
}
};
const signOut = async () => {
await supabase.auth.signOut();
setMessage("Signed out successfully!");
};
return (
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button onClick={signIn}>Sign In</button>
<button onClick={signUp}>Sign Up</button> {/* 新規登録ボタンを追加 */}
<button onClick={signOut}>Sign Out</button>
<p>{message}</p>
</div>
);
}
Step5.掲示板の実装(CRUD) + SupabaseとApp Routerでデータフェッチング
TypeScriptを使う際は型定義をしっかりとしておくことが重要だと実感...
export interface Post {
id: number;
title: string;
content: string;
user_id: string;
created_at: string;
updated_at: string;
}
'use client';
import { useEffect, useState } from "react";
import { supabase } from "../../lib/supabase";
import Auth from "../../components/Auth";
import { Session } from "@supabase/supabase-js";
import { Post } from "../../types/supabaseTypes";
export default function Dashboard() {
const [session, setSession] = useState<Session | null>(null);
const [posts, setPosts] = useState<Post[]>([]);
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
useEffect(() => {
// 現在のセッション情報を取得
const getSession = async () => {
const { data, error } = await supabase.auth.getSession();
if (data?.session) {
setSession(data.session);
fetchPosts(); // セッションが存在する場合に投稿データを取得
}
};
getSession();
// 認証の状態が変わった際にセッション情報を更新
const { data: authListener } = supabase.auth.onAuthStateChange(
(_event, sessionData) => {
if (sessionData && sessionData) { // sessionDataがnullでないことを確認
setSession(sessionData);
fetchPosts(); // セッションが変わった場合も投稿データを取得
} else {
setSession(null); // sessionDataがnullの場合はセッションをリセット
}
});
return () => {
authListener?.subscription.unsubscribe();
};
}, []);
const fetchPosts = async () => {
const { data } = await supabase.from("posts").select("*");
setPosts(data || []);
};
const createPost = async () => {
if (!session) return;
await supabase.from("posts").insert([
{ title, content, user_id: session.user.id },
]);
fetchPosts(); // 投稿を作成後、データを再取得
};
if (!session) {
return <Auth />;
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {session.user?.email}</p>
<div>
<h2>Your Posts</h2>
{posts.map((post) => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
</div>
<div>
<h2>Create New Post</h2>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
/>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Content"
/>
<button onClick={createPost}>Create Post</button>
</div>
</div>
);
}
DBにデータの登録とその表示ができるようになった!
Discussion