From 72e0140f01fa88a55c8feb1a202c37e391b534d4 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sat, 13 Jun 2026 14:35:11 -0400 Subject: [PATCH] feat(10-02): add first-login-wins is_admin bootstrap in upsertUser (D-01) - zero-admin COUNT check before INSERT: first user gets is_admin=true - subsequent users (admin already exists) get is_admin=false - existing-user early-return path unchanged (is_admin not modified) - Phase-12 hook comment: tighten to first login after app_config.setup_complete - adds 'import { sql }' from drizzle-orm --- apps/api/src/auth/user.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/api/src/auth/user.ts b/apps/api/src/auth/user.ts index 32c5507..3e3a383 100644 --- a/apps/api/src/auth/user.ts +++ b/apps/api/src/auth/user.ts @@ -8,7 +8,7 @@ * Source: RESEARCH.md § "User upsert with color assignment" */ -import { and, eq } from 'drizzle-orm'; +import { and, eq, sql } from 'drizzle-orm'; import { db } from '../db/client.js'; import { users } from '../db/schema.js'; @@ -109,7 +109,20 @@ export async function upsertUser(oidcIss: string, oidcSub: string, displayName?: COLOR_PALETTE.find((c) => !usedColors.has(c)) ?? COLOR_PALETTE[usedColors.size % COLOR_PALETTE.length]; - // 3. Insert new user row + // 3. First-login-wins is_admin bootstrap (D-01). + // When zero admins currently exist, the first new user becomes admin. + // Phase 12 will tighten this to: first user after app_config.setup_complete. + // Until then, "first user when zero admins exist" is the bootstrap condition. + // This hook reads cleanly: Phase 12 adds a setup_complete check before the + // COUNT, so only first login AFTER setup is flagged — no restructuring needed. + const [{ count }] = await db + .select({ count: sql`COUNT(*)` }) + .from(users) + .where(eq(users.isAdmin, true)) + .limit(1); + const shouldBeAdmin = Number(count) === 0; + + // 4. Insert new user row // mysql2 has no RETURNING clause — use $returningId() then re-select const [inserted] = await db .insert(users) @@ -118,10 +131,11 @@ export async function upsertUser(oidcIss: string, oidcSub: string, displayName?: oidcSub, displayName: displayName ?? null, color, + isAdmin: shouldBeAdmin, }) .$returningId(); - // 4. Re-select to return the full typed row + // 5. Re-select to return the full typed row const [newUser] = await db.select().from(users).where(eq(users.id, inserted.id)).limit(1); return newUser;