コンテンツにスキップ

0007. can_drive 能力フラグ (ロールと運転能力の分離)

コンテキストと課題

実務では「事務職 (normal) だが運転も担当する」「管理者 (admin) だが運転もする」社員がいる。こうした 社員を配車のドライバー候補・ドライバー画面の対象に含めたい。しかし roledriver に変えてしまうと、 事務・管理の権限を失ってしまう。ロールはそのままに「運転できる」能力だけを付与 したい。

決定

ロールとは独立した能力フラグ can_drive を導入する。「運転できる」判定は次の単一規則:

運転できる = role = 'driver'
           OR ((role = 'normal' OR role = 'admin') AND can_drive = true)
  • drivercan_drive の値に依存せず常に運転可
  • normal / admincan_drive = true のときだけ運転可 (admin は付与しても管理権限を一切失わない)。

実装:

  • DB: profiles.can_drive boolean not null default false を追加 (migration 51)。既定 false なので 誰も自動的に運転可にはならない (管理者が明示付与して初めて有効)。既存 driver 行は値を true に バックフィル (アプリ判定上は不要だが一覧の整合のため)。
  • 判定の権威は JWT (auth.users.raw_app_meta_data.can_drive)。RoleGuard・ナビ・配車候補が リフェッチ無しで判定できるよう、能力判定は app_metadata.can_drive を読む。profiles.can_drive は メンバー一覧表示・整合用の 副次コピー で、app_metadata と同時に service_role 経由の API (POST /team/members/:id/can-drive) で更新される。
  • 唯一の判定関数: canActAsDriver(role, canDrive) (AuthContext.tsx)。配車のドライバー候補フィルタ・ RoleGuard(/driver アクセス・allowDriverCapable)・ナビ (本日のルート表示) はすべてこれを使う。
export function canActAsDriver(role: UserRole | null, canDrive: boolean): boolean {
  return role === 'driver' || ((role === 'normal' || role === 'admin') && canDrive === true);
}

影響 (Consequences)

良い点

  • ロールを変えずに運転能力を付与でき、「事務 + 運転」「管理 + 運転」を権限を失わず表現できる。
  • 「運転できる」判定が canActAsDriver 1 関数に集約され、配車候補・ルートガード・ナビが同じ基準で 一致する。全組合せはテーブルテストで網羅されている。
  • 能力判定を JWT (app_metadata.can_drive) から読むため、画面側はメンバー情報を再取得せず判定できる。

トレードオフ / 注意点

  • 二重保存の同期責務: 権威の app_metadata.can_drive と副次コピーの profiles.can_drive を 常に同時更新する必要がある。これは service_role API (/team/members/:id/can-drive) に閉じている。
  • can_drive 変更も他のクレーム同様 トークン再取得まで反映されない (ADR 0003)。
  • 「ロール」と「能力」という概念が増えるため、ドライバー対象者を判定するコードは必ず canActAsDriver を経由する規律が要る (生の role === 'driver' 比較は不可)。

根拠 (典拠)

  • supabase/migrations/00000000000051_profiles_can_drive.sql:1-40 — 設計 (運転可 = driver OR (normal AND can_drive))、can_drive boolean not null default false、判定の権威は app_metadata.can_drive・本列は副次コピー (POST /team/members/:id/can-drive で同時更新)、driver 行のバックフィル。
  • apps/web/src/contexts/AuthContext.tsx:46-60,87,174canActAsDriver(driver OR ((normal|admin) AND canDrive))、canDrive コンテキスト、readAppMetaBoolean(user, 'can_drive')
  • apps/web/src/contexts/AuthContext.test.ts:68-88canActAsDriver の全組合せテーブルテスト (admin+canDrive=true → true 等) と配車候補フィルタ。
  • apps/web/src/components/auth/RoleGuard.tsx:17-43allowDriverCapableresolveRoleAccess(canActAsDriver を OR 評価)。