mirror of
https://github.com/tiennm99/bsk.git
synced 2026-09-05 12:16:42 +00:00
- supabase/migrations/20260525163400_bsk_admin.sql:
bsk.claim_first_admin(uuid) -> boolean, VOLATILE SECURITY DEFINER.
Advisory lock keyed by hashtext('bsk:claim_first_admin')::bigint
serializes concurrent first-sign-ins; EXISTS-guarded INSERT means
only the first caller wins.
- types/supabase-bsk.ts: added claim_first_admin to bsk.Functions.
- lib/auth/invite-schema.ts: InviteUserSchema (Zod v4: email + role
enum derived from appRoles) + InviteUserState discriminated union.
- app/[locale]/(app)/admin/invite/{actions,page,form}.tsx: admin-only
invite Server Action + page + RHF/useActionState client form.
Caller-role check via getServerSession() (defense in depth; the
(app)/admin layout in phase 06 will gate at the route level).
Insert uses createSupabaseAdminClient() because app_users has no
INSERT RLS policy by design.
- app/[locale]/(auth)/sign-in/actions.ts: extended enrollment-check
branch — when no row AND count == 0, calls claim_first_admin RPC.
On true, re-fetches enrollment row and proceeds; on false (race
lost) or count > 0, falls through to existing sign-out + generic
error (enumeration defense preserved).
- messages/{vi,en}.json: admin.invite.* keys (parity).
- docs/runbooks/first-admin-setup.md: happy path + manual psql
fallback bootstrap procedure.
No audit_log refs — trimmed plan respected.
46 lines
1.7 KiB
Markdown
46 lines
1.7 KiB
Markdown
# First-Admin Setup Runbook
|
|
|
|
## Happy Path (automatic)
|
|
|
|
The first person to successfully sign in when `bsk.app_users` is empty
|
|
is automatically granted the `admin` role.
|
|
|
|
Flow (implemented in `signInAction` + migration `20260525163400_bsk_admin.sql`):
|
|
|
|
1. User signs in with valid credentials.
|
|
2. `signInAction` checks for an enrollment row in `bsk.app_users`.
|
|
3. If no row exists, it reads `COUNT(*)` from `bsk.app_users`.
|
|
4. If count is 0, it calls `bsk.claim_first_admin(user_id)`.
|
|
5. The SQL function acquires `pg_advisory_xact_lock(hashtext('bsk:claim_first_admin')::bigint)`,
|
|
then inserts `(user_id, 'admin')` only if the table is still empty.
|
|
6. Returns `true` → sign-in proceeds as admin. Returns `false` (race lost) → generic error.
|
|
|
|
Two simultaneous first sign-ins: exactly one succeeds; the other receives
|
|
"Invalid email or password" and can retry (their next sign-in will find
|
|
count > 0 and be rejected as unenrolled until an admin invites them).
|
|
|
|
## Manual Fallback (psql)
|
|
|
|
Use this if the automatic claim ever fails or you need to bootstrap
|
|
a specific user directly.
|
|
|
|
```sql
|
|
-- 1. Find the user's UUID in auth.users
|
|
SELECT id, email FROM auth.users WHERE email = 'your@email.com';
|
|
|
|
-- 2. Insert the admin enrollment row
|
|
INSERT INTO bsk.app_users (user_id, role)
|
|
VALUES ('<uuid-from-step-1>', 'admin');
|
|
```
|
|
|
|
Run via Supabase dashboard SQL editor or `psql` with the connection string
|
|
from your Supabase project settings.
|
|
|
|
Note: deleting all rows from `bsk.app_users` effectively resets the
|
|
bootstrap — the next sign-in will claim admin again.
|
|
|
|
## Reference
|
|
|
|
Migration: `supabase/migrations/20260525163400_bsk_admin.sql`
|
|
Sign-in action: `app/[locale]/(auth)/sign-in/actions.ts`
|