Phase 10 — Admin Role & Settings (ADMIN-01/02/03) #17
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* GET /api/me — returns the authenticated user's identity and assigned color.
|
||||
* GET /api/me — returns the authenticated user's identity, admin role, and provider setup status.
|
||||
*
|
||||
* Flow (normal — OIDC active):
|
||||
* 1. getAuth(c) reads iss + sub from the OIDC session JWT cookie
|
||||
@@ -7,36 +7,72 @@
|
||||
* 2. Derives displayName from OIDC claims (name → preferred_username → email → sub fallback)
|
||||
* then calls upsertUser(iss, sub, displayName) — writes on first visit, corrects a
|
||||
* previously blank displayName on subsequent visits (idempotent, keyed on iss+sub, D-10)
|
||||
* 3. Returns { user: { id, displayName, color } }
|
||||
* 3. Queries users.isAdmin and member_credentials existence for the resolved user
|
||||
* 4. Returns { user: { id, displayName, color, isAdmin, needsProviderSetup } }
|
||||
*
|
||||
* Flow (dev bypass — DEV_AUTH_BYPASS=true, non-production):
|
||||
* devAuthBypass() injects DEV_USER via c.set('user', DEV_USER). oidcAuthMiddleware
|
||||
* is NOT mounted in index.ts when the bypass is active, so getAuth(c) is never called.
|
||||
* This handler reads c.get('user') first and short-circuits to return the dev identity
|
||||
* directly, skipping the DB upsert.
|
||||
* This handler reads c.get('user') first and short-circuits using the dev user's id,
|
||||
* but STILL queries the DB for isAdmin (T-10-05: bypass skips OIDC, not the DB check)
|
||||
* and member_credentials existence.
|
||||
*
|
||||
* Security (D-03, T-10-06):
|
||||
* isAdmin is exposed for UX-only PWA nav gating — it is NOT the security boundary.
|
||||
* The server enforces the role on every /api/admin/* request via requireAdmin (Plan 03).
|
||||
*
|
||||
* The OIDC session cookie is httpOnly + Secure + SameSite (T-02-03).
|
||||
* No credential or refresh-token data is included in the response (T-02-04).
|
||||
* No credential or refresh-token data is included in the response (T-02-04, T-10-07).
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { getAuth } from '../auth/middleware.js';
|
||||
import { upsertUser, deriveDisplayName } from '../auth/user.js';
|
||||
import { db } from '../db/client.js';
|
||||
import { users, memberCredentials } from '../db/schema.js';
|
||||
// Side-effect import: brings in the ContextVariableMap augmentation for c.get('user')
|
||||
import '../auth/devBypass.js';
|
||||
|
||||
export const meRouter = new Hono();
|
||||
|
||||
/**
|
||||
* Looks up isAdmin and needsProviderSetup for a given userId.
|
||||
* Always reads from the DB — bypass only skips OIDC, not this check (T-10-05).
|
||||
*/
|
||||
async function resolveAdminAndSetupStatus(userId: number) {
|
||||
const [userRow] = await db
|
||||
.select({ isAdmin: users.isAdmin })
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1);
|
||||
|
||||
const [cred] = await db
|
||||
.select({ id: memberCredentials.id })
|
||||
.from(memberCredentials)
|
||||
.where(eq(memberCredentials.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
return {
|
||||
isAdmin: userRow?.isAdmin ?? false,
|
||||
needsProviderSetup: !cred,
|
||||
};
|
||||
}
|
||||
|
||||
meRouter.get('/', async (c) => {
|
||||
// Dev-auth bypass path: devAuthBypass() sets c.get('user') to DEV_USER when active.
|
||||
// Return the injected dev identity directly — no DB round-trip, no OIDC session needed.
|
||||
// Use the injected dev identity's id for DB lookups — no OIDC session needed,
|
||||
// but isAdmin and needsProviderSetup are still resolved from the DB (T-10-05).
|
||||
const devUser = c.get('user');
|
||||
if (devUser) {
|
||||
const { isAdmin, needsProviderSetup } = await resolveAdminAndSetupStatus(devUser.id);
|
||||
return c.json({
|
||||
user: {
|
||||
id: devUser.id,
|
||||
displayName: devUser.displayName,
|
||||
color: devUser.color,
|
||||
isAdmin,
|
||||
needsProviderSetup,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -64,11 +100,15 @@ meRouter.get('/', async (c) => {
|
||||
return c.json({ error: 'Could not resolve user' }, 500);
|
||||
}
|
||||
|
||||
const { isAdmin, needsProviderSetup } = await resolveAdminAndSetupStatus(user.id);
|
||||
|
||||
return c.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
displayName: user.displayName,
|
||||
color: user.color,
|
||||
isAdmin,
|
||||
needsProviderSetup,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user