認証とアクセス制御
アプリがユーザー認証を行い、ロールでアクションを制御する方法。
このモノレポでは認証は中央集約されています。アプリ自身はBetter Authを組み込んだり、パスワードを保存したりしません。共有Auth UIがサインインを処理し、アプリはミドルウェア経由でセッションを確認するだけです。ロールベースの制限(例: 「マネージャーだけがレシピを編集できる」)は共有データベーステーブルを使って上に重ねます。
アーキテクチャ
3つのピースが連携します:
| ピース | 場所 | 役割 |
|---|---|---|
| Auth API | shared/auth/auth-api/ | Better Auth + Postgres + Resend(マジックリンク)。システムサービス。編集しない。 |
| Auth UI | shared/auth/auth-ui/ | サインインページ、管理コンソール、Auth APIへのプロキシ。 |
| あなたのアプリ | apps/<your-app>/ | ミドルウェア経由でAuth UIを呼び出すだけ。Better Authコードは不要。 |
templates/web/テンプレートから作成されたアプリには、認証対応済みのmiddleware.tsがすでに含まれています。通常は触る必要がありません。
セッションチェックの流れ
テンプレートのミドルウェアはリクエストごとに:
- ブラウザのcookieを転送しつつ
GET ${AUTH_UI_URL}/api/auth/get-sessionを呼び出す。 - 未認証で
AUTH_REQUIRED=trueなら、${AUTH_UI_URL}/?returnTo=<current-url>へリダイレクト。 - サインイン後、Auth UIがユーザーを戻し、
.up.railway.appにセッションcookieが設定されて永続化される。
環境変数
| 変数 | 設定場所 | 目的 |
|---|---|---|
AUTH_UI_URL | デプロイ時に自動注入 | Auth UIサービスのベースURL。 |
AUTH_REQUIRED | 環境ごとの設定 | 本番/ステージングはデフォルトtrue、必要に応じて環境ごとに上書き。 |
AUTH_UI_URLを自分で設定する必要はありません — デプロイワークフローが非認証サービスすべてに自動注入します。詳しくは環境変数を参照。
プレビューでの認証テスト
- PRを開き、プレビューURLが投稿されるコメントを待つ。
- プレビューURLにアクセス → ステージングのAuth UIへリダイレクトされる。
- サインイン(マジックリンク)。
- プレビューアプリに戻され、セッションcookieが設定される。
カスタムドメインは不要です — プレビューはすべて*.up.railway.appで動きます。
ロールベースのアクセス制御(きめ細かい制御)
「ロールXだけがアクションYを実行できる」(例: マネージャーだけがレシピを作成・編集できる)が必要なときは、セッションの上に権限チェックを重ねます。
ロールは共有のauthテーブルに入っていて、アプリ自身がロールenumを定義することはありません:
| テーブル | 目的 |
|---|---|
allowed_user | サインインできるユーザー。role(admin/user)とオプションのrole_idを持つ。 |
auth_role | "manager", "chef"のような名前付きロール。管理者が定義。 |
auth_role_app | どのロールがどのアプリを開けるか。 |
権限ヘルパー
メールアドレスで検索してbooleanを返すヘルパーをapp/_lib/auth.tsに1つ置きます:
app/_lib/auth.tstypescript
import { getDb } from '@/app/_lib/db';
export async function checkIsManager(email: string): Promise<boolean> {
const db = getDb();
const result = await db.query<{ base_role: string; role_name: string | null }>(
`SELECT au.role AS base_role, ar.name AS role_name
FROM allowed_user au
LEFT JOIN auth_role ar ON au.role_id = ar.id
WHERE au.email = $1`,
[email]
);
if (!result.rowCount) return false;
const row = result.rows[0];
return row.base_role === 'admin' || row.role_name === 'manager';
}フラグの使い方
- Server Component:
const isManager = await checkIsManager(session.user.email)でフラグを取得し、クライアントコンポーネントにpropsで渡す — ブラウザ側でチェックを再実装しない。 - API route(POST/PUT/DELETE): ロールがない場合は
403 Forbiddenを返す。 - UI: 条件付きレンダリング
{isManager && <Link href="/recipes/new">新しいレシピ</Link>}。
リファレンスアプリ
apps/nani-cooking/がエンドツーエンドで実装しています。コピーして参考にできるファイル:
app/_lib/auth.ts—checkIsManagerヘルパーapp/api/recipes/route.ts— マネージャー限定APIapp/recipes/new/page.tsx— マネージャー限定ページcomponents/RecipeCard.tsx—isManagerをpropsで受け取る
Claudeに聞く
アクションにロール制限を追加
Claudeプロンプトmeal-plannerアプリで、「manager」ロールを持つユーザーだけがレシピを作成・削除できるようにしてください。app/_lib/auth.tsに権限ヘルパーを追加、APIルートを保護、マネージャーでない場合はUIの「新しいレシピ」ボタンを非表示にしてください。nani-cookingのパターンに従ってください。
クイズ
Quiz
`manager`や`chef`のようなカスタムロール名はどこに存在しますか?