feat(01-02): wire OIDC middleware, /api/me route, and authenticated PWA shell
- src/auth/middleware.ts: re-exports oidcAuthMiddleware, processOAuthCallback,
getAuth from @hono/oidc-auth; documents required env vars and AUTH-02
refresh-token rotation (no iframe, D-12)
- src/routes/me.ts: GET / calls getAuth → upsertUser(iss, sub, email) →
returns { user: { id, displayName, color } }; identity keyed on iss+sub
- src/index.ts: /callback registered before oidcAuthMiddleware; /api/*
guarded; /health remains unauthenticated; /api/me mounted
- apps/pwa/src/api/client.ts: typed fetchMe() with credentials: 'include'
- apps/pwa/src/App.tsx: useQuery(['me'], fetchMe); renders member name and
color swatch; retains /health stack indicator from Plan 01
- tsc --noEmit: clean; all tests pass
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* GET /api/me — returns the authenticated user's identity and assigned color.
|
||||
*
|
||||
* Flow:
|
||||
* 1. getAuth(c) reads iss + sub from the OIDC session JWT cookie
|
||||
* (validated and refreshed by oidcAuthMiddleware — never reaches here unauthenticated)
|
||||
* 2. upsertUser(iss, sub, email) writes the user row on first visit, returns
|
||||
* the existing row on subsequent visits (idempotent, keyed on iss+sub, D-10)
|
||||
* 3. Returns { user: { id, displayName, color } }
|
||||
*
|
||||
* 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).
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono'
|
||||
import { getAuth } from '../auth/middleware.js'
|
||||
import { upsertUser } from '../auth/user.js'
|
||||
|
||||
export const meRouter = new Hono()
|
||||
|
||||
meRouter.get('/', async (c) => {
|
||||
// getAuth returns null only if the session is invalid — oidcAuthMiddleware on
|
||||
// /api/* redirects unauthenticated requests before this handler is reached.
|
||||
const auth = await getAuth(c)
|
||||
if (!auth) {
|
||||
return c.json({ error: 'Unauthorized' }, 401)
|
||||
}
|
||||
|
||||
// iss and sub are the stable identity fields; email is a display hint only (D-10)
|
||||
const iss = (auth.iss as string | undefined) ?? ''
|
||||
const sub = auth.sub ?? ''
|
||||
const email = typeof auth.email === 'string' ? auth.email : undefined
|
||||
|
||||
const user = await upsertUser(iss, sub, email)
|
||||
|
||||
if (!user) {
|
||||
return c.json({ error: 'Could not resolve user' }, 500)
|
||||
}
|
||||
|
||||
return c.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
displayName: user.displayName,
|
||||
color: user.color,
|
||||
},
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user