Phase 10 — Admin Role & Settings (ADMIN-01/02/03) #17

Merged
luckberg merged 25 commits from gsd/phase-10-admin-role-settings into main 2026-06-13 17:10:47 -04:00
Showing only changes of commit 72e0140f01 - Show all commits
+17 -3
View File
@@ -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<number>`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;