認証と権限 Auth & permissions
管理者ポータルは Supabase Auth を使った JWT ベース認証と、ロール/権限キーによる画面側フィルタリングで構成されています。
The admin portal uses Supabase Auth for JWT-based login, plus a role / permission-key system that filters the UI client-side.
ログインフロー Login flow
sequenceDiagram participant U as Admin participant FE as Admin Portal participant SB as Supabase Auth participant W as Workers BFF
/v1/admin/* participant DB as PostgreSQL U->>FE: email + password FE->>SB: signInWithPassword() (data-plane 例外: SDK 直叩き) SB-->>FE: session (JWT) Note over FE: AuthContext.signIn → verifyAdminRole()
RequireAuth ルートマウント時にも再検証 FE->>W: GET /v1/admin/admins/me (Bearer JWT) W->>DB: SELECT admins WHERE user_id = jwt.sub AND status='active' W-->>FE: 200 { admin } / 401・403・404 → signOut('not_admin') FE->>W: GET /v1/admin/admins?status=active (PermissionProvider) FE->>W: GET /v1/admin/role-permissions W->>DB: SELECT * FROM admin.role_permissions JOIN admin.roles DB-->>W: rows W-->>FE: admins[] + role_permissions マップ FE-->>U: Dashboard (nav は usePermission(key) で絞り込み) Note over FE,W: 以降のリクエストは全て BFF 経由
Authorization: Bearer {jwt} で JWT を渡す
service_role / DB 直叩きは UI バンドル外
関連コンポーネント Components involved
src/auth/AuthContext.tsx— セッション管理、signIn/signOut、verifyAdminRole()による/v1/admin/admins/me再検証、アイドルタイムアウト(VITE_IDLE_TIMEOUT_MIN/ dev デフォルト 1440 分)、PostHog identify、onAuthStateChange。Owns the session,signIn/signOut, role re-verification via/v1/admin/admins/me, the idle-timeout watcher (VITE_IDLE_TIMEOUT_MIN; dev default 1440 min), PostHog identify, and theonAuthStateChangelistener.src/auth/RequireAuth.tsx— ルートラッパ。マウント時にも/v1/admin/admins/meを叩いて role 再検証。401/403/404 はsignOut('not_admin')→/login。LocalStorage 復元経路でも admin role を必ず通る。Route wrapper. Always re-hits/v1/admin/admins/meon mount; 401/403/404 →signOut('not_admin')and back to/login. Catches LocalStorage-restored sessions that bypasssignIn.src/hooks/PermissionProvider.tsx—bff.admin.admins.list({ status: 'active' })+fetchAllRolePermissions()の 2 並列で role → permission Set マップを構築。usePermission(key)hook がtrue/falseを返す。fail-open(移行期): 未ログイン / 権限ロード失敗時はtrueを返し UI を割らない。専用の permissions endpoint(/v1/admin配下、仮称admins/me/permissions)は未実装(TODO H-A6)。Builds the role → permission Set map by fetchingadmins.list({ status: 'active' })andfetchAllRolePermissions()in parallel.usePermission(key)returns true/false. Fail-open (migration phase): when unauthenticated or the load fails it returnstruerather than locking everyone out. A dedicated permissions endpoint (under/v1/admin, tentativelyadmins/me/permissions) is still a TODO (H-A6).src/lib/supabase.ts— Supabase Auth 用クライアントの初期化のみ(anonkey、DB 直叩きはしない)。CRUD は全て@parky/bff-clientから/v1/admin/*を叩く。Auth 管理操作(パスワード発行など)も BFF (POST /v1/admin/admins) 内でservice_roleを使う構成。Initializes only the Supabase Auth client (anon key; no direct DB access). All CRUD goes through@parky/bff-clientto/v1/admin/*. Privileged Auth admin actions (password provisioning, etc.) live inside the BFF (POST /v1/admin/admins) where theservice_rolekey is held.
service_role キーは強力です。**ポータル UI バンドルには絶対に含めない**。Workers (BFF) の Secret として保管し、特権操作 (Auth admin / 直接 SQL 等) は全て BFF サーバ側で行う。
The service_role key is powerful and **must never be bundled into the portal UI**. It lives only as a Workers (BFF) secret; all privileged operations (Auth admin actions, direct SQL, etc.) happen server-side inside the BFF.
ロール Roles
ロールは roles テーブルで定義され、各ロールに対する権限キーは role_permissions で管理されます。
Roles live in the roles table, with permission keys per role stored in role_permissions.
| Role | 想定担当Intended owner | アクセス範囲Scope |
|---|---|---|
| Super Admin | システム管理者Platform admin | 全権限All permissions |
| Ops Manager | 運用マネージャーOps manager | 駐車場・ユーザー・売上・サポートParking, users, revenue, support |
| Content Manager | コンテンツ担当Content team | 記事・広告・ゲーミ・カスタマイズArticles, ads, gamification, theming |
| Support Agent | CS担当Customer support | サポートチケット・誤情報報告Support tickets + error reports |
権限キーマトリクス Permission key matrix
31 個の権限キーが定義されており、Roles 画面でチェックボックスマトリクスとして一括管理します。変更は保存ボタン押下時にのみ DB 反映(オートセーブ禁止)。SSoT は web/portal/admin/src/pages/RolesPage.tsx の allPermissions。
31 permission keys, managed as a checkbox matrix on the Roles screen. Changes commit only on Save — never on autosave. The source of truth is allPermissions inside web/portal/admin/src/pages/RolesPage.tsx.
| グループGroup | キーKeys |
|---|---|
| メイン / Main | dashboard.view |
| 駐車場 / Parking | parking.view · parking.edit · tags.view · tags.edit · reviews.view · reviews.moderate |
| ユーザー / Users | users.view · users.edit |
| オーナー / Owners | owners.view · owners.edit |
| 運営 / Ops | plans.view · plans.edit · support.view · support.respond · notifications.view · notifications.send · sales.view |
| コンテンツ / Content | articles.view · articles.edit · articles.publish · ads.view · ads.edit · gamification.view · gamification.edit |
| 管理者 / Admins | admins.view · admins.edit · roles.view · roles.edit |
| システム / System | settings.view · settings.edit |
RBAC ロード & 判定フロー RBAC load & evaluation flow
flowchart LR
Start((App boot)) --> Auth{Supabase
session?}
Auth -- no --> Login[/login/]
Auth -- yes --> Verify[GET /v1/admin/admins/me]
Verify -- 401/403/404 --> Logout[signOut 'not_admin']
Verify -- 200 --> Load[PermissionProvider]
Load --> A1[bff.admin.admins.list]
Load --> A2[fetchAllRolePermissions]
A1 --> Match[email 一致で
自分の admin 行を特定]
A2 --> Map[role_id → Set permission_key]
Match --> Hold[PermissionContext]
Map --> Hold
Hold --> Hook[usePermission key]
Hook -- has key --> OK[render]
Hook -- missing & loaded --> Hide[UI hide / Forbidden]
Hook -- loading or fail --> Open[fail-open: render
BFF が二次防御]
アクセス制御の実装レイヤ Enforcement layers
- クライアント:
AuthContextが権限キーを保持、サイドバー / ボタンをhasPermission('parking.edit')で表示制御。 - Client:
AuthContextexposes permission keys; the sidebar and action buttons gate onhasPermission('parking.edit'). - RLS ポリシー: Supabase の Row Level Security を各テーブルに設定し、テーブル単位でアクセスを制御。
- RLS policies: Supabase Row Level Security on each table restricts access server-side.
- service_role: 管理者アカウント作成や削除など、特権操作は **BFF サーバ側 (Workers) でのみ**
service_roleを使用する。フロントは BFF (POST /v1/admin/admins等) を叩くだけでservice_roleキーは UI バンドルに入らない。 - service_role: Privileged flows (create / delete admin) use
service_role**inside the BFF (Workers) only**. The portal calls BFF endpoints (POST /v1/admin/adminsetc.); theservice_rolekey is never bundled into the UI.
管理者アカウント作成フロー Admin account creation flow
sequenceDiagram participant SA as Super Admin participant FE as Admins page participant W as Workers BFF
POST /v1/admin/admins participant Auth as Supabase Auth (service_role) participant DB as admins table SA->>FE: Fill form + submit FE->>W: POST /v1/admin/admins
{ email, name, role_id } W->>W: Generate 12-char random password (server-side) W->>Auth: admin.createUser(email, password, email_confirm: true) Auth-->>W: user (id = user_id) W->>DB: INSERT into admin.admins (user_id, name, email, role_id, status='active') DB-->>W: row W-->>FE: { admin, initial_password } FE-->>SA: Display initial password (one-time, copy button) Note over W,Auth: service_role キーは BFF 内のみで保持
UI バンドルには含めない
初期パスワードは画面上でのみ表示され、閉じた後は再表示できません。
リセット時は resetAdminPassword() が新しいパスワードを生成 → Auth 更新 → 画面表示 の流れになります。
The initial password is shown once and never again. A reset flow regenerates a password, updates Auth, and surfaces the new value via resetAdminPassword().