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
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
* Source: RESEARCH.md § "User upsert with color assignment"
|
* 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 { db } from '../db/client.js';
|
||||||
import { users } from '../db/schema.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.find((c) => !usedColors.has(c)) ??
|
||||||
COLOR_PALETTE[usedColors.size % COLOR_PALETTE.length];
|
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
|
// mysql2 has no RETURNING clause — use $returningId() then re-select
|
||||||
const [inserted] = await db
|
const [inserted] = await db
|
||||||
.insert(users)
|
.insert(users)
|
||||||
@@ -118,10 +131,11 @@ export async function upsertUser(oidcIss: string, oidcSub: string, displayName?:
|
|||||||
oidcSub,
|
oidcSub,
|
||||||
displayName: displayName ?? null,
|
displayName: displayName ?? null,
|
||||||
color,
|
color,
|
||||||
|
isAdmin: shouldBeAdmin,
|
||||||
})
|
})
|
||||||
.$returningId();
|
.$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);
|
const [newUser] = await db.select().from(users).where(eq(users.id, inserted.id)).limit(1);
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
|
|||||||
Reference in New Issue
Block a user