feat(03-09): convert resolveUserId to async — real OIDC iss/sub→users.id via upsertUser (CR-06)
- Import upsertUser from auth/user.js - resolveUserId now async: dev-bypass path unchanged; OIDC path calls getAuth then upsertUser(iss, sub, email) to resolve DB user id - All 5 handlers (create, edit, delete, sync-status, writable-calendars) updated to await resolveUserId and 401 only when it returns null - Remove all inline 'For now return 401' stubs and redundant getAuth calls - grep confirms 0 'For now return 401' stubs remain; upsertUser imported+called
This commit is contained in:
@@ -30,6 +30,7 @@ import { db } from '../db/client.js'
|
|||||||
import { calendarEvents, calendars, users, calendarOutbox } from '../db/schema.js'
|
import { calendarEvents, calendars, users, calendarOutbox } from '../db/schema.js'
|
||||||
import { expandOccurrences } from '../broker/expand.js'
|
import { expandOccurrences } from '../broker/expand.js'
|
||||||
import { getAuth } from '../auth/middleware.js'
|
import { getAuth } from '../auth/middleware.js'
|
||||||
|
import { upsertUser } from '../auth/user.js'
|
||||||
// Side-effect import: brings in the ContextVariableMap augmentation for c.get('user')
|
// Side-effect import: brings in the ContextVariableMap augmentation for c.get('user')
|
||||||
import '../auth/devBypass.js'
|
import '../auth/devBypass.js'
|
||||||
|
|
||||||
@@ -43,15 +44,30 @@ const MAX_WINDOW_DAYS = 90
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Auth helper — shared by all write endpoints
|
// Auth helper — shared by all write endpoints
|
||||||
// Returns the numeric userId from dev-bypass context; null if not present.
|
//
|
||||||
// The OIDC path requires a separate getAuth(c) call — only the dev-bypass path
|
// Resolution order (D-10, CR-06):
|
||||||
// injects c.get('user'). Write handlers check this first, then fall back to getAuth.
|
// 1. Dev-bypass path: c.get('user') is set by devAuthBypass() middleware when
|
||||||
// ---------------------------------------------------------------------------
|
// DEV_AUTH_BYPASS=true. Return its .id directly — no OIDC round-trip.
|
||||||
|
// 2. OIDC path: call getAuth(c). If null → unauthenticated, return null.
|
||||||
|
// Otherwise extract iss/sub/email and call upsertUser — which writes the
|
||||||
|
// user row on first login and returns the existing row on subsequent calls.
|
||||||
|
// Identity is keyed on oidc_iss + oidc_sub (D-10), never email.
|
||||||
|
// 3. Callers emit 401 when resolveUserId returns null.
|
||||||
|
//
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function resolveUserId(c: any): number | null {
|
async function resolveUserId(c: any): Promise<number | null> {
|
||||||
const devUser = c.get('user') as { id: number } | undefined
|
const devUser = c.get('user') as { id: number } | undefined
|
||||||
if (devUser) return devUser.id
|
if (devUser) return devUser.id
|
||||||
return null
|
|
||||||
|
const auth = await getAuth(c)
|
||||||
|
if (!auth) return null
|
||||||
|
|
||||||
|
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)
|
||||||
|
return user?.id ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -196,16 +212,8 @@ eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
|
|||||||
// Does NOT build a VEVENT and does NOT call Fastmail — that is the worker's job (D-12).
|
// Does NOT build a VEVENT and does NOT call Fastmail — that is the worker's job (D-12).
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
eventsRouter.post('/create', zValidator('json', eventFieldsSchema), async (c) => {
|
eventsRouter.post('/create', zValidator('json', eventFieldsSchema), async (c) => {
|
||||||
// Auth: dev bypass first, then OIDC session.
|
const currentUserId = await resolveUserId(c)
|
||||||
const currentUserId = resolveUserId(c as Parameters<typeof resolveUserId>[0])
|
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)
|
||||||
if (currentUserId === null) {
|
|
||||||
// Fall through to getAuth for OIDC path
|
|
||||||
const auth = await getAuth(c)
|
|
||||||
if (!auth) return c.json({ error: 'Unauthorized' }, 401)
|
|
||||||
// In production OIDC path, we'd look up the user row by iss+sub.
|
|
||||||
// For now return 401 if OIDC auth is not backed by a DB user here.
|
|
||||||
return c.json({ error: 'Unauthorized' }, 401)
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = c.req.valid('json')
|
const payload = c.req.valid('json')
|
||||||
|
|
||||||
@@ -273,12 +281,8 @@ eventsRouter.post('/create', zValidator('json', eventFieldsSchema), async (c) =>
|
|||||||
// Returns 202 immediately (D-05). Does NOT call Fastmail (D-12).
|
// Returns 202 immediately (D-05). Does NOT call Fastmail (D-12).
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
eventsRouter.patch('/:uid/edit', zValidator('json', eventFieldsSchema), async (c) => {
|
eventsRouter.patch('/:uid/edit', zValidator('json', eventFieldsSchema), async (c) => {
|
||||||
const currentUserId = resolveUserId(c as Parameters<typeof resolveUserId>[0])
|
const currentUserId = await resolveUserId(c)
|
||||||
if (currentUserId === null) {
|
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)
|
||||||
const auth = await getAuth(c)
|
|
||||||
if (!auth) return c.json({ error: 'Unauthorized' }, 401)
|
|
||||||
return c.json({ error: 'Unauthorized' }, 401)
|
|
||||||
}
|
|
||||||
|
|
||||||
const uid = c.req.param('uid')
|
const uid = c.req.param('uid')
|
||||||
const payload = c.req.valid('json')
|
const payload = c.req.valid('json')
|
||||||
@@ -377,12 +381,8 @@ eventsRouter.patch('/:uid/edit', zValidator('json', eventFieldsSchema), async (c
|
|||||||
// Returns 202 immediately (D-05). Does NOT call Fastmail (D-12).
|
// Returns 202 immediately (D-05). Does NOT call Fastmail (D-12).
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
eventsRouter.delete('/:uid', async (c) => {
|
eventsRouter.delete('/:uid', async (c) => {
|
||||||
const currentUserId = resolveUserId(c as Parameters<typeof resolveUserId>[0])
|
const currentUserId = await resolveUserId(c)
|
||||||
if (currentUserId === null) {
|
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)
|
||||||
const auth = await getAuth(c)
|
|
||||||
if (!auth) return c.json({ error: 'Unauthorized' }, 401)
|
|
||||||
return c.json({ error: 'Unauthorized' }, 401)
|
|
||||||
}
|
|
||||||
|
|
||||||
const uid = c.req.param('uid')
|
const uid = c.req.param('uid')
|
||||||
|
|
||||||
@@ -442,12 +442,8 @@ eventsRouter.delete('/:uid', async (c) => {
|
|||||||
// Returns { uid, status: 'done' } when no outbox row exists (nothing pending = settled).
|
// Returns { uid, status: 'done' } when no outbox row exists (nothing pending = settled).
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
eventsRouter.get('/sync-status', zValidator('query', syncStatusQuerySchema), async (c) => {
|
eventsRouter.get('/sync-status', zValidator('query', syncStatusQuerySchema), async (c) => {
|
||||||
const currentUserId = resolveUserId(c as Parameters<typeof resolveUserId>[0])
|
const currentUserId = await resolveUserId(c)
|
||||||
if (currentUserId === null) {
|
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)
|
||||||
const auth = await getAuth(c)
|
|
||||||
if (!auth) return c.json({ error: 'Unauthorized' }, 401)
|
|
||||||
return c.json({ error: 'Unauthorized' }, 401)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { uid } = c.req.valid('query')
|
const { uid } = c.req.valid('query')
|
||||||
|
|
||||||
@@ -495,12 +491,8 @@ eventsRouter.get('/sync-status', zValidator('query', syncStatusQuerySchema), asy
|
|||||||
// Response: { calendars: [{ url, displayName, color, isShared }] }
|
// Response: { calendars: [{ url, displayName, color, isShared }] }
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
eventsRouter.get('/writable-calendars', async (c) => {
|
eventsRouter.get('/writable-calendars', async (c) => {
|
||||||
const currentUserId = resolveUserId(c as Parameters<typeof resolveUserId>[0])
|
const currentUserId = await resolveUserId(c)
|
||||||
if (currentUserId === null) {
|
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)
|
||||||
const auth = await getAuth(c)
|
|
||||||
if (!auth) return c.json({ error: 'Unauthorized' }, 401)
|
|
||||||
return c.json({ error: 'Unauthorized' }, 401)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// D-03 writable set: own personal calendars + shared Family calendar.
|
// D-03 writable set: own personal calendars + shared Family calendar.
|
||||||
|
|||||||
Reference in New Issue
Block a user