diff --git a/src/servers/sidecar/email/http.ts b/src/servers/sidecar/email/http.ts index a41b5d68..1411aa5d 100644 --- a/src/servers/sidecar/email/http.ts +++ b/src/servers/sidecar/email/http.ts @@ -1,4 +1,5 @@ import { Hono } from 'hono'; +import { getUserById } from 'officerdb'; import { emailRouter } from './routes'; // The email sidecar's own listener. `/api/email/*` on the platform is a proxy onto this — the platform @@ -12,12 +13,24 @@ type HonoVariables = { user: { id: number }; body: Record; orig const app = new Hono<{ Variables: HonoVariables }>(); -// The platform's userMiddleware set `user` from the JWT. Here it comes from the header the proxy injects — -// trusted because this server binds loopback only and nothing else can reach it. +// The platform's userMiddleware set the whole user row on the context from the JWT. The proxy injects only +// the id (X-Officer-User), so the rest is loaded here — and it has to be the WHOLE row, not just the id: +// the mail store is keyed by the owner's email (DATA_PATH//email_accounts//emails.db), so +// a user object without `email` silently resolves to the wrong path and the mailbox reads as empty. +// +// Cached because this runs on every request and the owner does not change; single-user is a hard invariant. +let cachedUser: { id: number; email: string } | null = null; + app.use('*', async (ctx, next) => { const id = Number(ctx.req.header('X-Officer-User')); if (!Number.isFinite(id) || id <= 0) return ctx.json({ error: 'missing X-Officer-User' }, 401); - ctx.set('user', { id } as never); + + if (!cachedUser || cachedUser.id !== id) { + const row = await getUserById(id); + if (!row?.email) return ctx.json({ error: `unknown user ${id}` }, 401); + cachedUser = row as { id: number; email: string }; + } + ctx.set('user', cachedUser as never); await next(); });