1 連絡・概要
- 課題2 についての連絡があります。
1.1 今後の流れ
今回の講義の内容は、冬休みの宿題が完了していることを前提したものになります。
ここまでの開発によって、ローカル環境に構築した「ブログアプリ」について、「投稿記事」と「カテゴリ」の CRUD操作 (=Create / Read / Update / Delete) に関して、フロントエンドとバックエンドの両方で概ね実装が完了したことになります。
次のステップとしては ユーザ認証機能 を実装していきます。これにより、管理者だけが (=ログインした特定ユーザだけが) 記事やカテゴリの新規作成・編集・削除を実行 できるような仕組みをつくっていきます。
また、次回講義では「画像のアップロード機能」を実装し、次々回の講義では Vercel というホスティングサービスに「ブログアプリ」をデプロイ (配置・展開) し、手持ちのスマートフォンなどから「ブログアプリ」にアクセスできるように公開していきます。
なお、上記のような「認証機能の実装」や「アプリのデプロイ」については、様々な選択肢がありますが、この授業ではSupabaseとVercelという2つのウェブサービス (クラウドプラットフォーム) を利用していきます。いずれのサービスも、学習目的 (≠商用利用) であれば無料利用可能な「枠」が存在し、一定の制限 (例えば、作成可能なプロジェクト数の制限など) の範囲内であれば クレジットカードの登録も不要 で利用できます。感謝。
2 準備
管理者用の各種画面にアクセスするためのポータル画面として、プロジェクトに
src/app/admin/page.tsx を作成して、次のコードを貼り付けておいてください。
"use client";
import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
const Page: React.FC = () => {
return (
<main>
<div className="mb-2 text-2xl font-bold">管理者用機能の一覧</div>
<ul>
<li>
<FontAwesomeIcon icon={faArrowRight} className="mr-2" />
<Link className="text-blue-500 underline" href="/admin/posts">
/admin/posts
</Link>
</li>
<li>
<FontAwesomeIcon icon={faArrowRight} className="mr-2" />
<Link className="text-blue-500 underline" href="/admin/posts/new">
/admin/posts/new
</Link>
</li>
<li>
<FontAwesomeIcon icon={faArrowRight} className="mr-2" />
<Link className="text-blue-500 underline" href="/admin/categories">
/admin/categories
</Link>
</li>
<li>
<FontAwesomeIcon icon={faArrowRight} className="mr-2" />
<Link
className="text-blue-500 underline"
href="/admin/categories/new"
>
/admin/categories/new
</Link>
</li>
</ul>
</main>
);
};
export default Page;開発者モードでアプリを起動し、/admin
にアクセスすると次のような画面が表示されることを確認してください。
3 プロジェクトのビルド
冬休みの宿題を含めると、これまでに next-blog-app プロジェクト に対して 約30~40 個のファイルを作成・追加してきたことになります。
開発中は、主に npm run dev
コマンドを使って「開発モード」でアプリを起動し、エラーやワーニングが生じていないことを確認していると思います。しかし、ここから先の開発をはじめる前に、本番用
(プロダクション用)
に「ビルド」を実行し、プロジェクトに問題がないことを改めて確認
していきます。
第06回講義で一度解説したように、以下のコマンドを使って本番環境用(プロダクション環境用)にアプリをビルドすることができます。
npm run build
以上のコマンドを実行して、そのログ (コンソール出力) に エラーやワーニングが報告されないこと を確認してください。もし問題があれば、生成AIなどを利用して解決してください (ワーニングについては、必要に応じて無視も可能です)。
なお、ビルド結果は .next
フォルダに出力されます。以下のコマンドを使って本番モード (プロダクションモード)
でアプリの実行が可能です。
npm run start
4 認証機能 (ログイン機能) の実装
認証機能 (ログイン機能) を実装していきます。
ここではSupabaseという BaaS (=Backend as a Service) を利用して認証機能を実装します。Supabase は、ウェブアプリ/ウェブサービスの開発で一般に必要となる「リレーショナルデータベース」「ファイルストレージ」「認証機能」「サーバレスファンクション」などのバックエンド機能を包括的に提供してくれるサービスとなっています。
(プロンプト例)
supabase とは、どのような機能を提供してくれるサービスですか。できるだけ専門用語をさけて分かりやすく教えてください。
このブログアプリ開発では、Supabaseの「認証機能」を投稿記事やカテゴリの新規作成・編集・削除の制限に利用する他に、「リレーショナルデータベース機能」を 投稿記事やカテゴリの管理 に利用、また「ファイルストレージ機能」を 記事に添付する画像 (CoverImg) の管理 に利用していきます。
データベースシステムの変更 ( SQLite→PostgreSQL)
ここまでの開発には、データベースとして「SQLite」を使用してきました。しかし、SQLiteは ファイルベースのデータベース であり、本番環境で使用するためには Node.js から直接読み書き可能な永続的なディスク領域を持ったサーバ環境 が必要になります。しかし、そのようなサーバ環境を 無料提供してくれるホスティングサービスは限られているため、以降は、Supabase の無料枠のなかで利用可能な PostgreSQL という RDBMS (Relational DataBase Management System) に移行します。
なお、PostgreSQL は、エンタープライズレベルの本番環境で広く採用されている高機能なオープンソースデータベースシステムです。SQLite が書き込み操作を1つのみに制限するのに対し、PostgreSQLはトランザクション制御により 複数ユーザーからの同時アクセスを安全に処理すること ができます。
4.1 Supabase 登録とプロジェクト作成
Supabase を利用するためには、はじめに「ユーザ登録」が必要になります。Freeプランであれば、クレジットカードの登録不要 で利用可能です。以下の手順で「ユーザ登録」と「組織・プロジェクトの作成と設定」を行なってください。
Supabase の公式ホームページ (https://supabase.com/) にアクセスして「Start your project」のボタンを押下してください。
Supabaseは ソーシャルログイン機能 (ソーシャル認証) を提供しており、「GitHubアカウント」を使ったサインインが可能になっています。この機能を使ってサインインするために「Continue with GitHub」ボタンを押下してください。
ソーシャルログインにおける 認可スコープ (=Supabase が利用する GitHubアカウントの情報と権限) の確認画面に遷移するので、内容を確認のうえ「Authorize supbase」ボタンを押下してください。なお、複数のGitHubアカウントを持っている場合は、下記の画面で、どのGitHubアカウントで連携するのかを確認・選択してください。
つづいて、以下のような 組織 (Organization) の新規作成を行なう画面「Create a new organization」に移動します。
Supabase における「組織」とは「チームメンバーの管理や料金請求の単位」を指します。無料の「Freeプラン」では、1つの組織を作成して、そこに 最大2つまでのプロジェクト (Project) が作成可能になっています。各プロジェクトでは 1個 のデータベース を持つことができます。
Plan の項目が「Free - $0/month」になっていることを確認して「Create organization」のボタンを押下してください。
つづいて、以下のようなプロジェクトの作成画面に移動します。
Project Name として Next-Blog-App
を設定して、Region (地域) として必ず Northeast Asia (Tokyo) を選択してください。初期設定 (=
Asia-Pacific) のままにすると シンガポールのデータセンタ
にリソース (データベースなど)
が作成され、利用時の応答時間が非常に長くなるので注意してください。
また「Generate a
password」のリンクを押下して、データベース用のパスワードを自動生成してください。なお、このパスワードは「データベース接続文字列」として、環境変数の設定ファイル
.env にも記述するので 普段使いのパスワードを使用すること はお勧めしません。
パスワードの生成ができたら、以下のように「Copy」のボタンを押下して安全な場所 (ローカルのテキストファイルなど) に貼り付けておいてください。このあとの手順で使用します。
パスワードをメモしたら「Create new project」のボタンを押下してください。
プロジェクトの「トップページ」に移動します。
この際、もし「Setting up project」のような表示でているときは、しばらく待機してください。以上の手続きで Supabase の データベース機能 (PostgreSQL) が実際に利用可能になりました。
4.2 Prismaのデータベース接続情報の更新
現在、プロジェクトにおいて ORM (O/R Mapper) である Prisma
は、プロジェクトローカルに配置した prisma/dev.db (SQLiteデータベース)
と接続するように設定されています。これを Supabase
(クラウドプラットフォーム) に構築された PostgreSQL に切り替えるように設定 (接続情報)
を変更していきます。
4.2.1 PostgreSQL用の Prisma アダプタのインストール
第08回講義では、ORM である Prisma と
SQLite3 を接続するアダプタ (橋渡し役) として @prisma/adapter-better-sqlite3
というライブラリをインストールしました。ここからは、接続先を supabase (PostgreSQL)
に変更するので、それにあわせたアダプタをインストールします。また、Node 側の PostgreSQL
ドライバ pg もインストールします。
VSCode のコンソールから以下のコマンドを実行してください。
npm i @prisma/adapter-pg pg
4.2.2 schema.prisma の書き換え
つづいて SQLite3 から PostgreSQL への接続切り替えを prisma/schema.prisma
に記述しています。次のように provider の項目を "sqlite" から
"postgresql" に変更してください。
変更後は、忘れずに保存してください。
4.2.3 src/lib/prisma.ts の書き換え
つづいて、Next.js のなかで Prisma クライアントの初期化・生成を担当している
src/lib/prisma.ts にも、SQLite3 から PostgreSQL
への切り替え反映していきます。
現在の Prisma クライアントの初期化処理 (シングルトンとして管理) を…
import { PrismaClient } from "@/generated/prisma/client";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};
const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL || "file:./dev.db",
});
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
adapter,
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}次のように変更してください。
import { PrismaClient } from "@/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg"; // 更新
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); // 変更
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
adapter,
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}変更後、忘れずに保存してください。
(プロンプト)
シングルトンパターンとは何ですか。Prismaクライアントの初期化は「Next.js では、Prisma クライアントはシングルトンとして管理することが推奨される」と言われたのですが意味不明です。
4.2.4 prisma.config.ts の書き換え
プロジェクトルートの prisma.config.ts にも、同様に SQLite3 から PostgreSQL
への切り替えを反映させいきます。このファイルは、Next.js
のフレームワークから参照されるものでなく、VSCode のターミナルから
npx prisma db push や npx prisma db seed
のコマンドを実行したときに参照される設定になります (npx prisma ...
の実行時に参照される Prisma CLI 用の設定です)。
現在の設定を…
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: "tsx prisma/seed.ts",
},
datasource: {
url: env("DATABASE_URL"),
},
});次のように変更してください。
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: "tsx prisma/seed.ts",
},
datasource: {
url: env("DIRECT_URL"),
},
});変更後、忘れずに保存してください。
4.2.5 環境変数 .env の書き換え
まずは Supabase のウェブページから、接続情報 (DB接続文字列) を取得します。次のように画面上部の「 Connect」のボタンを押下してください。
ダイアログが表示されるので「ORMs」を選択肢します。
次のように Tool が「Prisma」になっていることを確認して、「.env.local」タブを選択し、「Copy」ボタンを押下して 環境変数の設定 をコピーしてください。
注意
supabase のサイトでは、DB接続文字列を .env.local
に設定するように例示されていますが、現在のプロジェクトでは .env
に設定するので注意してください。
VSCodeで、プロジェクトルートの環境変数の設定ファイル .env
を開いてください。
現在、コメント部分を除けば、.env
は以下のような内容になっているはずです。今後、microCMS API
は使用しないので、それに関連する設定は「削除」または「コメントアウト」しておいてください。
NEXT_PUBLIC_MICROCMS_API_KEY=xxxxxxxxxxxxxx
NEXT_PUBLIC_MICROCMS_BASE_EP=https://xxxxxxxx.microcms.io/api/v1
DATABASE_URL="file:./dev.db"これを以下のように変更してください。先ほど Supabase のサイトで「コピー」したテキストを貼り付け、それを加筆修正してください。この設定を間違うと、DB接続に失敗するので慎重に細心の注意を払って設定してください。
# Connect to Supabase via connection pooling
DATABASE_URL="postgresql://postgres.XXXX:YYYY@aws-1-ap-northeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1"
# Direct connection to the database. Used for migrations
DIRECT_URL="postgresql://postgres.XXXX:YYYY@aws-1-ap-northeast-1.pooler.supabase.com:5432/postgres"XXXの部分は「データベースを指定する文字列」になります。supabaseのサイトでコピーしたものをそのまま使ってください。YYYの部分は「Create a new project」の画面で生成した パスワード に置き換えてください。DATABASE_URLの末尾に&connection_limit=1を付加してください。
プロンプト例
Next.js + Prisma + supabase でウェブアプリを開発しています。また、最終的に Vercel にデプロイする予定です。そのようななかで、DB (supabase) の接続文字列
DATABASE_URLの末尾にはconnection_limit=1を設定したほうがよい、という情報がありました。この設定の意味について解説してください。
.gitignore の内容を再確認
既に第06回講義 において設定しているハズですが、再度
.gitignore のなかに .env が含まれていること (.env が
GitHub にアップロードされないようになっていること) を再確認してください。
注意
Freeプランで作成した Supabase のプロジェクトは、7日間、非アクティブ状態が継続すると (=データベースや認証機能などに対するアクセスがないと)「paused」という状態になってしまいます。この状態では、Supabase の API を叩いても機能しないので注意 してください。
プロジェクトが「paused」になってしまった場合は、ブラウザから Supabase にアクセスして解除することができます (解除には数分かかることがあります)。アプリ開発中は7日間放置することはないと思いますが、完成後は、この状態になることがあるので十分に注意してください。特に、ポートフォリオとして開発したウェブアプリのURLを提出する際には、注意してください。
「paused」に切り替わったときは、以下のようなメールが届きます (メールのなかのリンクからアクセスしたページで「アクティブ状態」に戻すことができます)。
Hi there,
To save on cloud resources we’re currently pausing free-tier projects that are inactive for more than 7 days.
Your project XXXXXX has been paused.
You can unpause it from the dashboard within the next 90 days. Beyond that point, you won’t be able to unpause your project, but you’ll be able to download your data. For more information read the docs.
If you want, you can upgrade your organization to Pro to avoid future pausings, and this can be done from the billing settings.
▼▼ paused になってしまったプロジェクト
▼▼ 解除のための操作
paused なプロジェクトを選択すると、以下のダイアログが表示されるので「Restore project」を選択します。
4.3 データベース接続とテーブル作成 (初期化)
まずは、更新された設定に基づき、Prisma を改めて初期化します。VSCodeのターミナルから次のコマンドを実行してください。
npx prisma generate
次のような応答が返ってくれば問題ありません。失敗するときは、ここまでの設定変更を再確認してください (ファイルの 保存忘れ などに注意してください)。
Loaded Prisma config from prisma.config.ts.
Prisma schema loaded from prisma\schema.prisma
✔ Generated Prisma Client (7.1.0) to .\src\generated\prisma in 69ms
次に、supabase が提供するデータベース (PostgreSQL) を初期化していきます。SQLite の場合と同様に Primsa 経由でテーブルの作成と初期化 をします。VSCodeのターミナルから次のコマンドを実行してください。
npx prisma db push
次のような応答 (Your database is now in sync with your Prisma schema)
が返ってくれば、Supabaseに構築したDBの初期化処理 (つまり Post、Category、PostCategory
という3つのテーブルの作成)
が成功したことになります。失敗するときは、ここまでの設定変更を再確認してください。
Loaded Prisma config from prisma.config.ts.
Prisma schema loaded from prisma\schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "aws-1-ap-northeast-1.pooler.supabase.com:5432"
The database is already in sync with the Prisma schema.
各テーブルが作成されたことを Supbase で確認してみます。以下の手順で「Table Editor」にアクセスしてテーブルが作成されていることを確認してください。
RLS (Row Level Security) の有効化
詳細は後述しますが、現状 (初期状態) では、RLS というものが無効化されています。これが無効な場合、データベースが無防備な状態となっています。以下の手順で、各テーブルの RLS を有効化しておいてください。
(プロンプト例)
supabase が提供するデータベースを利用しているのですが、RLS とはなんですか。RLSが「無効」になっていると、無防備な状態になると聞いたのですが詳しく説明してください。
ブログアプリからも、 Supabase のテーブルにレコードが追加できること
を確認してみます。アプリを開発モードで起動して
(npm run dev)、実際に記事を投稿してみてください。
Supbase から Post
テーブルを確認してください。次のようにレコードが追加されていれば成功🎉です
(反映されていないときは画面をリロードしてください)。
以上が確認できれば、SQLite から PostgreSQL への切り替えは無事完了したことになります。
Supabaseの「Schema Visualizer」機能について
Supabase の「Schema
Visualizer」という機能を使用して、データベースのスキーマ (=構造やテーブル間の関係)
を視覚的に確認することができます。実際に確認してみてください
(テーブルの位置はマウスドラッグで変えることができます)。ユーザーが作成したテーブルは、すべて
public というスキーマ内で管理されます。
次に、上記の ❸ のドロップダウンリストを auth に切り替えてみてください。
先に説明しているように Supabase
は「ユーザ認証機能」も提供しますが、その認証に使っているテーブル構造が確認できます。認証機能は複数のテーブルで構成されており、ユーザーの基本情報を管理する
users テーブルや、セッション情報を扱う sessions
テーブルのほか、多要素認証やパスワードリセットなどの高度なセキュリティ機能をサポートするための補助テーブルが使われています。
4.4 テーブルの削除
テーブルに新しくカラムを追加した場合など、prisma/schema.prisma
を変更したときは、再度 npx prisma db push を実行する必要があります。
- 本番運用の開始後は
npx prisma migrateという「既存レコードを残したままスキーマを変更するコマンド」を使用してください。
npx prisma db push を再実行する際には、事前に既存テーブルを削除 しておくことが推奨されます。SQLite
を使用していたときは dev.db
を削除することでテーブルを削除することができました。一方、Supabase の
PostgreSQL では、次のような SQL文を実行してテーブルを削除する必要があります
(GUIを使ってテーブルを削除することも可能です)。
大文字を含むテーブルを指定するときは、ダブルクォーテーションで囲む必要がある
ので注意してください。また、public.
というスキーマ指定も必要になります。具体的には、次のような手順で、Supabase の「SQL
Editor」から SQL文 を実行してください (実際に試してみてください)。
「Run」ボタンを押下すると (Ctrl+Enter
を入力すると)、次のような確認ダイアログが表示されます。これは、テーブルの削除などの破壊的な操作を含むSQLを実行するときに表示されるダイアログになります。内容を確認して「Run
this query」を押下してください。
「Table Editor」の画面に移動し、次のように テーブルが削除できていること を確認してください。
4.5 テーブル作成とRLS設定
削除したテーブルを再作成します (npx prisma generate
も忘れずに実行してください)。また、さらに、第08回講義で実行したようにprisma/seed.ts
を使って 初期データを投入 します。
VSCodeに戻って、ターミナルから以下のコマンドを実行してください。
npx prisma db push
npx prisma generate
npx prisma db seed
Supabase の「Table Editor」で、次のように初期データの投入を含めて問題なく実行できていることを確認してください。
また、この画面において以下の図のように、テーブル名の右に「UNRESTRICTED」が表示され、テーブルのステータスが「RLS disabled」になっていることを確認してください。ここで RLS とは Row Level Security の頭文字をとったもので テーブルの行単位 (レコード単位) での読み書きを制限する仕組み・機能 になります。
この RLS を無効 (disabled) のまま放置すると、ANON_KEY を設定した
SupabaseClient
(@supabase/supabase-jsで導入してフロントエンドで動作するもの) を通じて テーブルレコードの読み取り、編集、削除などが制限なく可能
になってしまうという問題があります。ANON_KEY
は、フロントエンドプログラムから読み取り可能であるため、少しの知識があれば、それを読み取って悪用可能であり、非常に大きなセキュリティリスク
(脆弱性) となってしまいます。
この問題を解決するために「SQL Editor」から、次のような SQL を実行して 全てのテーブルで RLS を有効化 してください (先に示したように、GUIからも RLS の有効化ができます)。成功すると「Success. No rows returned」のログが出力されます。
ALTER TABLE public."Post" ENABLE ROW LEVEL SECURITY;
ALTER TABLE public."Category" ENABLE ROW LEVEL SECURITY;
ALTER TABLE public."PostCategory" ENABLE ROW LEVEL SECURITY;
再び Table Editor に戻って「RLS が有効になっていること」を確認してください (反映されていないときは、ページをリロードしてください)。
4.6 ログイン可能なユーザの追加
Supabase の認証機能を使って メールアドレス (ログインID) と パスワード を使って認証可能なユーザを作成していきます。この処理はウェブアプリ上に「サインアップ (ユーザ登録)」のページを実装して、そこから行なうこともできます。しかし、このブログアプリでは 管理者1名だけの認証ができれば問題ない ので、Supabase のサイトから手動でユーザを登録していきます。
次のように「Add user」のボタンを押下し、「Create new user」を選択してください。
次のようなダイアログが表示されるので、適当なメールアドレスとパスワードを設定して、「Create user」のボタンを押下してください。
なお「Auto Confirm
User?」にチェックを入れていると、「認証メールの送信プロセス」が省略されるので、メールアドレスは架空のもの
(admin@example.com など) でも問題ありません。
次のように、ユーザが追加されたことを確認してください。
4.7 フロントエンドで使用するAPIキーの取得
ブログアプリのフロントエンドプログラムにおいて、SupabaseClient
(@supabase/supabase-js) を使って Supabase
の「認証機能」や「PostgreSQL」にアクセスするための APIキー
を取得します。なお、バックエンドプログラムでは 基本的に Prisma
経由 で PostgreSQL にアクセスします。
以下の2つのキーを取得してください。
▼ NEXT_PUBLIC_SUPABASE_URL の取得
▼ NEXT_PUBLIC_SUPABASE_ANON_KEY の取得
注意
ここで取得する APIキー NEXT_PUBLIC_SUPABASE_ANON_KEY
は、ウェブブラウザで動作する「フロントエンドプログラム」から参照する値であり、その性質上、利用者に読み取られる可能性があります。そのため、以下のような注意が書かれています。
This key is safe to use in a browser if you have enabled Row Level Security for your tables and configured policies.
(訳) テーブルの Row Level Security (RLS) を有効化し、適切なポリシーを設定している場合、このキーはフロントエンド環境(ウェブブラウザ)でも安全に使用可能です。
VSCode に戻って次のように環境変数の設定ファイル .env を編集してください。
# Connect to Supabase via connection pooling
DATABASE_URL="postgresql://postgres.XXXX:YYYY@aws-1-ap-northeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1"
# Direct connection to the database. Used for migrations
DIRECT_URL="postgresql://postgres.XXXX:YYYY@aws-1-ap-northeast-1.pooler.supabase.com:5432/postgres"# Connect to Supabase via connection pooling
DATABASE_URL="postgresql://postgres.XXXX:YYYY@aws-1-ap-northeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1"
# Direct connection to the database. Used for migrations
DIRECT_URL="postgresql://postgres.XXXX:YYYY@aws-1-ap-northeast-1.pooler.supabase.com:5432/postgres"
NEXT_PUBLIC_SUPABASE_URL=https://ZZZZ.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=WWWWなお、環境変数のうち NEXT_PUBLIC_ から始まる値は フロントエンドで参照可能な値
になります。利用者に読み取られても問題のない値だけ、NEXT_PUBLIC_
を付けるようにしてください。バックエンドでは全ての値を参照可能です。
4.8 SupabaseClient のインストール
VSCode のターミナルから次のコマンドを実行して、SupabaseClient を使用するために必要なライブラリをインストールしてください。
npm i @supabase/supabase-js
また、プロジェクトに src/utils/supabase.ts
を新規作成して、以下の内容を記述してください。これは、環境変数を読み取って
SupabaseClient (=データベース操作用のAPI操作ツール)
のインスタンスを生成、初期化するプログラムになります。
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);以降、import { supabase } from "@/utils/supabase"
のようにインポートすれば、SupabaseClient である supabase を通じて Supabase の各種機能 (認証機能やデータベース) を利用すること
ができます。
4.9 ログインページの実装
ブログアプリに、IDとパスワードを使った「ログインページ」を実装していきます。
プロジェクトに src/app/login/page.tsx を新規作成してこちらのプログラムを貼り付けて、保存してください。
さらに、src/app/_components/ValidationAlert.tsx
を新規作成して、以下のプログラムを貼り付けて、保存してください。これは
login/page.tsx
から参照しているコンポーネントで、バリデーションエラーを表示するためのものになります。
"use client";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleExclamation } from "@fortawesome/free-solid-svg-icons";
const ValidationAlert = ({ msg }: { msg: string }) => {
if (msg === "") {
return null;
}
return (
<div className="flex items-center space-x-1 text-sm font-bold text-rose-400">
<FontAwesomeIcon icon={faCircleExclamation} className="mr-0.5" />
<div>{msg}</div>
</div>
);
};
export default ValidationAlert;開発モードでアプリを起動して /login
にアクセスすると、次のような画面が表示されることを確認してください。
この画面で、誤ったメールアドレスとパスワードを入力して「ログイン」を押下すると、次のようになることを確認してください。また、その際、ブラウザのデベロッパーツール (F12で起動) のコンソールタブの出力も確認 してください。SupabaseClient が (バックグラウンドで) SupabaseAPI にリクエストを送って 400 Bad Request がレスポンスされていることが確認できます。また、認証に失敗時には SupabaseClient から返されるエラーオブジェクトの内容を確認することができます。
▼ ブラウザのデベロッパーツールのコンソール出力
次に Supabase のユーザ管理画面で設定したメールアドレスとパスワードを入力して「ログイン」のボタンを押下すると /admin に「リダイレクト (=ページ移動)」 することを確認してください。
4.10 ログイン処理の解説
src/app/login/page.tsx
の内容について解説していきます。やや長めのプログラムですが、ログイン処理に関する本質的な部分は、次の箇所になります。
まず、第07行目で src/app/utils/supabase.ts から
SupabaseClient をインポートしています。
「ログイン」のボタンが押下されると、第29行目 の handleSubmit
関数がコールされ、UI/UX関連の処理後に、次のコードが実行されます。
try {
console.log("ログイン処理を実行します。");
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
setLoginError(
`ログインIDまたはパスワードが違います(${error.code})。`
);
console.error(JSON.stringify(error, null, 2));
return;
}
console.log("ログイン処理に成功しました。");
router.replace("/admin");
} catch (error) {
setLoginError("ログイン処理中に予期せぬエラーが発生しました。");
console.error(JSON.stringify(error, null, 2));
} finally {
setIsSubmitting(false);
}メールアドレスとパスワードを使ったログイン (サインイン) を試みているのが
第36行目 の supabase.auth.signInWithPassword
メソッドになります。ログインに成功すると error には null
が格納され、失敗すると以下のような エラーオブジェクト
が格納されます (以下には error
オブジェクトをJSONに変換したものを掲載しています)。
error が null ではないとき (=つまり認証に失敗したとき) は
第40行目 の if 文が実行され、第45行目 の return
により handleSubmit
関数を抜けています。一方で、認証に成功したときは、第48行目
の処理によって /admin のページに リダイレクト
(=ページ移動) するようになっています。
この router.replace(...) は、以下のように 第08行目
でインポートして、第17行目 で取得している useRouterフック
のメソッドになります。useRouter は ページ遷移を操作するための機能セット を提供するものになります。
supabase.auth.signInWithPassword() によるログインに成功すると、Supabaseからは
アクセストークン (Access Token) と呼ばれる JWT
(JsonWebToken) 形式の文字列 が発行され、その他の情報と共に、
ウェブブラウザのローカルストレージ に sb-XXXX-auth-token
というキーで 認証情報 が保存されます (XXXX の部分には
Supabase の ProjectID が入ります)。このアクセストークン (以下、トークン)
とは、認証の結果、つまり、認証済みであることの証明書
としての役割を持ちます。次回講義では「バックエンドにおけるユーザ認証の手順」を解説しますが、そこでトークンを利用することになります。
ローカルストレージに保存された情報 (トークンを含む) は、デベロッパーツール
(F12で起動) を使って、以下のように確認できます。access_token が
トークン (JWT)
になります。なお、後述の「ログアウト処理」を実行すると、この認証情報はローカルストレージから削除されます。
一見すると、トークン (JWT) はランダムな文字列のように見えますが、実体としては JSON を BASE64URLエンコードした文字列 と、その文字列に対して秘密鍵(JWT Secret)を用いて生成された 署名 (ハッシュ) を結合した文字列として構成されています。
- 署名 (ハッシュ) については、情報2 の 第09回講義の講義資料 を参照してください。
JWT (JSON Web Token) については、来年度の「知能情報実験実習2」の前期のテーマ のなかで詳しく学びます。
4.11 演習
トークン (JWT) は、JSON文字列をBASE64URLエンコードしてつくられているので、デコードすれば元のJSON が復元可能です。JWTの公式ページでは、そのデコード処理がブラウザ上で実行できるので、以下の手順で試してください。
- デベロッパーツールから
access_tokenを取得して (項目を選んで右クリックから「値をコピー」して)、その内容を以下のサイトに貼り付けてください。ログインしていない状態ではaccess_tokenが存在しないので注意してください。 - JWTの公式ページ (https://jwt.io/) にアクセスして、Encoded に値を貼り付けてください。
- Decoded の PAYLOAD から、復元された JSON の内容を確認してください。
JWTの「構成」や「署名の検証方法」などの仕組み (なぜ、これで安全な認証ができるのか、改竄が不可能なのか) について興味がある学生は、以下のウェブ記事や、生成AIなどを利用して学んでください。
- 初学者向け: JWT認証の流れを理解する@ Qiita
- 【JWT】入門@ Qiita
5 認証状態の取得・確認
前のセクションでは、SupabaseClient の auth.ignInWithPassword() メソッドに ID
と パスワード を与え、ユーザの認証 (Authentication)
ができることを確認しました。また、このメソッドは、認証に成功するとブラウザのローカルストレージに認証情報を格納し、失敗するとエラー情報を格納したオブジェクトを戻り値として返すことを確認しました。
しかし、現状では、ブラウザのアドレスバーに /admin を打ち込めば 認証
(Authentication)
の有無に関わらず「管理者用ページ」が表示できてしまいます。ここでは、認証されたユーザだけが「管理者用ページ」を表示できるようにするための
認可 (Authorization) という処理について学んでいきます。
認証 (Authentication) と認可 (Authorization)
言葉として非常に似ていますが、ウェブアプリ開発の文脈では、次のように区別されるので注意してください。
- 認証 (Authentication):
ユーザが「誰か」また「本人であるか」を確認する仕組みや処理のこと。
- 例えば
auth.signInWithPassword("admin@example.com","****")は、パスワードという「ユーザ本人しか知り得ない情報」を使って、ユーザがadmin@example.comであることを確認する「認証」の仕組みといえます。
- 例えば
- 認可 (Authorization): 認証されたユーザ (あるいは認証されていないユーザ)
について、特定のリソースやアクションにアクセスする権限を有するかを確認する仕組みや処理のこと。
- 例えば、
admin@example.comとして認証されたユーザが/adminや/admin/posts/newにアクセスすることが許可されているか を確認して、それに応じた処理をするのが「認可」になります。
- 例えば、
ここでは、認証済みユーザだけに /admin 配下のURLパス (/admin/*)
の表示と、それに関連するウェブAPI (/api/admin/*)
の利用を許可するようなシンプルな認可処理を実装していきます。
5.1 フロントエンドの認可、カスタムフックの定義
フロントエンドでは、SupabaseClient の auth.getSession()
メソッドで「現在のユーザーの認証状態」が確認できます。認証済みの場合は
セッションオブジェクト が、未認証の場合は null
が返されます。なお、セッションオブジェクトからは session.user.email
でユーザのメールアドレス (ログインID) が取得可能となっています。
しかし、src/app/admin/ 配下にある全ての page.tsx に
auth.getSession() 関連のロジックを個別に実装することは コードの重複を招き、保守性を著しく低下
させることになります。そのため、ここでは useAuth
という「カスタムフック」の作成と、layout.tsx
を用いた「ルートガード
(詳細は後述)」の実装により、認可処理を一元管理するようにしていきます。
まずは、認可関連の処理を「カスタムフック」と呼ばれる形式で実装します。認可処理を適切に行なうには
useState や useEffect などの ReactHooks
が必要になるのですが、これらは 通常の関数 (普通に定義した関数)
の内部では利用できない という制約があります。ReactHooks
を使用するときは「カスタムフック」という形式で、一定の規則・制約に従って処理を記述すること
が求められます (例えば「ファイル名」と「関数名」を useXXX.ts
の形式で設定するなどの規則に従う必要があります) 。
(プロンプト例)
Next.js + TypeScript でウェブアプリを開発しています。「カスタムフック」と「普通の関数」の違いについて教えてください。保守性と再利用性を考えて処理を分離したいと考えているのですが、どちらで実装するべきですか。
認可処理に使用する useAuth
というカスタムフックを作成します。src/app/_hooks/useAuth.ts
というファイルを作成して、以下のプログラムを貼り付けてください。
import { useState, useEffect } from "react";
import { Session } from "@supabase/supabase-js";
import { supabase } from "@/utils/supabase";
export const useAuth = () => {
const [isLoading, setIsLoading] = useState(true);
const [session, setSession] = useState<Session | null>(null);
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
// 初期セッションの取得
const initAuth = async () => {
try {
const {
data: { session },
} = await supabase.auth.getSession();
setSession(session);
setToken(session?.access_token || null);
setIsLoading(false);
} catch (error) {
console.error(
`セッションの取得に失敗しました。\n${JSON.stringify(error, null, 2)}`
);
setIsLoading(false);
}
};
initAuth();
// 認証状態の変更を監視
const { data: authListener } = supabase.auth.onAuthStateChange(
async (event, session) => {
setSession(session);
setToken(session?.access_token || null);
}
);
// アンマウント時に監視を解除(クリーンアップ)
return () => authListener?.subscription?.unsubscribe();
}, []);
return { isLoading, session, token };
};このカスタムフックは page.tsx や
layout.tsx、その他のカスタムフックのなかで、次のように使用します。
まずは、useAuth の「使用方法」から先に確認していきます。このフックは、次の3つの値を返します。
isLoadingは、認証状態の確認中は「true」となり、確認完了すると「false」になります。sessionには、認証済みユーザと確認されたときに セッション情報 (Sessionオブジェクト) が格納されます。そうでないときはnullとなります (初期状態、つまりisLoadingが「ture」のときもnullとなることに注意してください)。tokenには、認証済みユーザと確認されたとき、バックエンドの認可処理に使うためのアクセストークン (JWT (JsonWebToken) 形式の文字列) が格納されます。未認証または初期状態ではnullとなります。このtokenは第07回講義で学んだ microCMS の API と同様の方法で利用します。
想定する使い方としては、認可が必要なページにおいて、isLoading
が true のときは「Loading…」を表示し、これが false に切り替わったら
session を確認して、もし null なら トップページにリダイレクトし、そうでなければ コンテンツを出力
します。また、認可が必要なウェブAPIにリクエストする際、HTTP Header に token
を追加します。
次に useAuth の内部処理 src/app/_hooks/useAuth.ts を解説します。
まず、第06行目 から 第08行目
にかけて、useState を利用して 3つの状態
(isLoading、session、token) を定義します。初期状態では
isLoading は true、他は null になります。
これら3つの値を useState
を使って「状態」として管理することで、これらの値が更新されたときに、それを参照している「Reactコンポーネント」に
再レンダリングがかかる
というメリットがあります。通常の変数として定義すると、この便利な「更新検知の仕組み」が機能しないことに注意してください。
つづいて、第10行目 からの useEffect
は、大きく2つ役割を持ちます。まず、第1の役割として 認証状態の取得
を担います。useAuth() がコールされたタイミングで auth.getSession()
を実行し、その結果に応じて、3つの状態
(isLoading、session、token) を更新します。
第2の役割として、認証状態の変更監視
を設定しています。これにより、ログアウトなどによって認証状態が変化した場合、onAuthStateChange
イベントが発生して session と token
の状態が更新される仕組みをつくっています。また、コンポーネントのアンマウントされるときには、監視を解除して、メモリリークを防止しています。
(プロンプト例)
Next.js でウェブアプリを開発しています。コンポーネントの「マウント」と「アンマウント」とは何ですか。useEffect との関係を含めて解説してください。
第41行目では、3つの状態を単一のオブジェクトとして返却し、この useAuth() を使用するコンポーネントから参照できるようにしています。
5.2 useAuth を利用したヘッダの表示パターンの切り替え
認証状態に応じて、次のようにヘッダの表示パターンの切り替え処理 (未認証のときは「Login」、認証済みのときは「Logout」と表示する処理) を実装してきます。
このような処理を 出し分け や「条件分岐表示」「動的切り替え」のように表現します。
実装例を以下に示します。Headerコンポーネントは 既に各自でカスタマイズしているので思うので、必要な部分だけを書き換えるようにしてください。
"use client";
import Link from "next/link";
import { twMerge } from "tailwind-merge";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFish } from "@fortawesome/free-solid-svg-icons";
import { supabase } from "@/utils/supabase"; // ◀ 追加
import { useAuth } from "@/app/_hooks/useAuth"; // ◀ 追加
import { useRouter } from "next/navigation"; // ◀ 追加
const Header: React.FC = () => {
// ▼ 追加
const router = useRouter();
const { isLoading, session } = useAuth();
const logout = async () => {
await supabase.auth.signOut();
router.replace("/");
};
// ▲ 追加
return (
<header>
<div className="bg-slate-800 py-2">
<div
className={twMerge(
"mx-4 max-w-2xl md:mx-auto",
"flex items-center justify-between",
"text-lg font-bold text-white"
)}
>
<div>
<Link href="/">
<FontAwesomeIcon icon={faFish} className="mr-1" />
MyBlogApp
</Link>
</div>
<div className="flex gap-x-6">
{/* ▼ 追加 */}
{!isLoading &&
(session ? (
<button onClick={logout}>Logout</button>
) : (
<Link href="/login">Login</Link>
))}
{/* ▲ 追加 */}
<Link href="/about">About</Link>
</div>
</div>
</div>
</header>
);
};
export default Header;第07行目 で useAuth
をインポートして、第13行目 で useAuth() を実行し、その戻り値を分割代入しています (このコンポーネントでは
token を使用しないので、isLoading と session
だけを得ています)。
第38行目 から 第41行目 で、出し分け (条件分岐表示)
をしています。未認証のときは /login
ページにリンクする「Login」を表示し、認証済みの場合は 第14行目
で定義している「ログアウト処理」をコールする「Logout」を表示するようにしています。また、認証状態の確認中は、いずれも表示しない用にしています。
第14行目 では、ログアウト処理を定義しています。また、ログアウト後はトップページにリダイレクトするようにしています。
5.3 演習
開発者モードでアプリを起動し、ヘッダ部が期待するような動作をすることを確認してください。
- ログインしていない状態では、ヘッダ部に「Login」が表示され、クリックすると
/loginに移動する。 - ログイン済みの状態では、ヘッダ部に「Logout」が表示され、クリックすると「ログアウト処理」と「トップページへの移動処理」が実行される。
6 ルートガードの実装
ルートガード (Route Guard) とは 認証状態に基づいて特定のルート (URLパス) に対するアクセスを制御し、権限を持たないユーザを適切なページへリダイレクトする機能 を指します。
このブログアプリでは、/admin/*
へのアクセスがあったとき、ユーザ認証済みであればコンテンツを表示し、そうでなければ
ログインページにリダイレクト するようなルートガードを構築します。
ルートガードは、保守性を考慮して個々の page.tsx ではなく
src/app/admin/layout.tsx に設置します。第06回講義で解説したように
layout.tsx
は、それが配置される階層以下の全てのページに適用されるので
src/app/admin に配置すれば、/admin や /admin/posts/new
など /admin
以下の全てのアクセスにルートガードを適用することができるようになります。
📂 src/app/
├─ 📄 page.tsx
├─ 📄 layout.tsx
└─ 📂 admin/
├─ 📄 page.tsx
├─ 📄 layout.tsx # ここにルートガードを設置
├─ 📂 categories/
└─ 📂 posts/
├─ 📄 page.tsx
├─ 📂 [id]/
└─ 📂 new/
└─ 📄 page.tsx
src/app/admin/layout.tsx
を作成して、以下のプログラムを貼り付けてください。
"use client";
import React from "react";
import { useAuth } from "@/app/_hooks/useAuth";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
interface Props {
children: React.ReactNode;
}
const AdminLayout = ({ children }: Props) => {
const router = useRouter();
const { isLoading, session } = useAuth();
useEffect(() => {
// 認証状況の確認中は何もせずに戻る
if (isLoading) {
return;
}
// 認証確認後、未認証であればログインページにリダイレクト
if (session === null) {
router.replace("/login");
}
}, [isLoading, router, session]);
// 認証済みが確認できるまでは何も表示しない
if (!session) {
return null;
}
return <>{children}</>;
};
export default AdminLayout;開発者モードで起動して、未ログイン状態で /admin や
/admin/posts、/admin/posts/new にアクセスすると
(ウェブブラウザのアドレバーにパスを入力すると)、ログインページ (/login)
に移動すること、つまり、ルートガードが機能することを確認してください。
6.1 演習
再利用性と保守性を高めるために、ルートガードの処理を
src/app/_hooks/useRouteGuard.ts というカスタムフック
として分離し、それを src/app/admin/layout.tsx
から呼び出すようにしてください。
7 課題2
次の NN-課題2-氏名.docx
を雛形に、「リポジトリのURL」の他、以下の各ページの「スクリーンショット」と「画面や機能の解説・アピールポイント」を記載して提出してください。
注意: ワードファイルの編集には、ウェブ版の Office (Microsoft 365 for the web) ではなく、ローカルアプリ版の Office を使用してください。ウェブ版を使用すると体裁が大きく崩れます。また、提出後、Teams のプレビュー画面 (提出物の確認画面) では体裁が乱れることがありますが、ダウンロードして開いたときに体裁が乱れてなければ問題ありません。課題を評価する際は、ファイルをダウンロードしてローカルアプリ版で開いて閲覧します。
注意: スクリーンショットは、PC版 と スマートフォン版 (次のセクションで指定する方法で撮影) の「両方」をつけてください。
| URL | 処理 |
|---|---|
| /admin/categories | カテゴリの一覧表示、編集ページへのリンク、削除機能 (オプション) |
| /admin/posts | 投稿記事の一覧表示、編集ページへのリンク、削除機能 (オプション) |
| /admin/posts/[id] | 投稿記事の編集 (削除を含む) |
- 提出期限 : 01月14日 (水) 23時
- 課題の採点後は、修正版の再提出や追提出があっても原則として再評価はしません。
- 担当教員に事前相談することなく、提出期限から168時間(=1週間・7日間)を超過して提出されたもの(再提出を含む)は、未提出と同じ0点評価とします。
- 提出先 : Teams
- ファイル形式 :
- PDF変換せずにワードファイルのまま提出してください
(
.docx形式)。不備は一律で「3点減😭」とします。 - ファイル名は
NN-課題2-氏名.docxとしてください。NNは ゼロ埋め2桁の出席番号 (本年度のもの)、氏名はフルネームとして、ハイフンや数字は全て半角文字としてください。姓と名の間には スペースを入れない でください。不備は一律で「2点減😭」とします。- 例:
03-課題2-高専太郎.docx - 例:
16-課題2-寝屋川次郎.docx
- 例:
- PDF変換せずにワードファイルのまま提出してください
(
- 採点 :
「7点」を標準として「10点満点」で評価します。学年末の成績評価の際には「重み」が付けられます。
- Wordファイル上の体裁の乱れに注意してください。
- 画面表示やバリデーションなどの UI/UX の工夫、機能の追加、react-hook-form & zod を利用などがあれば、アピールポイントとして、ワードファイル内に記載してください。
- 提出された課題 (ワードファイル) はクラスの学生に共有します。
7.1 提出ファイル作成に関する注意事項
雛形のWordファイルは、ダウンロードして、必ず デスクトップアプリ版の Word を使って編集 してください。
ブラウザ版 (Word for the Web) や Teams版 で閲覧・編集すると、体裁や設定が崩れることがあります(減点対象)。ブラウザで開いてしまった場合は、次の手順でデスクトップアプリで開き直してください。
Word のオプションから「画像が自動圧縮されない設定になっていること」を確認してください。
7.2 スマホ版のスクショの撮影方法
課題に添付するスマートフォン表示のスクリーンショット (iPhone 16 Plus を想定) は、必ず以下の手順で取得してください。
名前を「iPhone 16 Plus」、画面サイズを「430x932」に設定したエミュレートデバイスを追加してください。
デバイスから「iPhone 16 Plus」を選択して、「Capture full size screenshot (フルサイズのスクリーンショットをキャプチャ)」を使って画面全体のスクリーンショットを取得してください。
- Zoom (ズーム) の値 (以下では
125%になっている部分) は、スクショサイズには影響しません。 - 必要に応じて 下部の余白はトリミング してください。