diff --git a/ka-note/.env.example b/ka-note/.env.example index 71f74bb..d1f2754 100644 --- a/ka-note/.env.example +++ b/ka-note/.env.example @@ -1,21 +1,20 @@ -AZURE_CLIENT_ID= +# ── SERVER ─────────────────────────────────────────────────────────────────── +PORT=9000 +DEV_AUTH_BYPASS=false +AI_LOCK_EXPIRY_HOURS=168 + +# Azure AD — server app registration (validates incoming JWTs) +AZURE_CLIENT_ID= AZURE_TENANT_ID= -# Set to true for local dev to skip JWT verification (never use in production) -# DEV_AUTH_BYPASS=true +# Graph / OBO — required for calendar integration +# App Registration → API permissions → Graph → Calendars.Read (delegated) → grant admin consent +# App Registration → Certificates & secrets → New client secret +AZURE_CLIENT_SECRET= +# aud of client token must match this. Only needed if frontend uses a different app registration. +AZURE_OBO_CLIENT_ID= -# AI lock duration in hours (default: 24, use 168 for 7 days) -# AI_LOCK_EXPIRY_HOURS=168 - -# Graph API calendar integration (OBO flow) -# AZURE_CLIENT_SECRET= -# AZURE_OBO_CLIENT_ID= -# Only needed if VITE_AZURE_CLIENT_ID != AZURE_CLIENT_ID (separate frontend/backend registrations). -# The OBO assertion token's audience must match this client_id. -# Required: App Registration → API permissions → Microsoft Graph → Calendars.Read (delegated) → Grant admin consent -# Then: Certificates & secrets → New client secret → copy value here - -# Client needs VITE_ prefix — create client/.env with: -# VITE_AZURE_CLIENT_ID= -# VITE_AZURE_TENANT_ID= -# VITE_DEV_AUTH_BYPASS=true ← DEV ONLY: skips MS login in browser (never set in production) +# ── CLIENT (Vite — copy relevant lines to client/.env) ─────────────────────── +# VITE_AZURE_CLIENT_ID= +# VITE_AZURE_TENANT_ID= +# VITE_DEV_AUTH_BYPASS=true # DEV ONLY — never set in production diff --git a/ka-note/server/ka-note.db-shm b/ka-note/server/ka-note.db-shm index fe9ac28..d202222 100644 Binary files a/ka-note/server/ka-note.db-shm and b/ka-note/server/ka-note.db-shm differ diff --git a/ka-note/server/ka-note.db-wal b/ka-note/server/ka-note.db-wal index e69de29..73b2e57 100644 Binary files a/ka-note/server/ka-note.db-wal and b/ka-note/server/ka-note.db-wal differ diff --git a/ka-note/server/src/lib/graph-service.ts b/ka-note/server/src/lib/graph-service.ts index d8920b6..95c0def 100644 --- a/ka-note/server/src/lib/graph-service.ts +++ b/ka-note/server/src/lib/graph-service.ts @@ -21,11 +21,17 @@ const oboClientId = process.env.AZURE_OBO_CLIENT_ID ?? process.env.AZURE_CLIENT_ const clientSecret = process.env.AZURE_CLIENT_SECRET ?? ''; async function getOboToken(userToken: string, userId: string): Promise { + console.log(`[graph] getOboToken userId=${userId} tenantId=${tenantId || '(missing)'} oboClientId=${oboClientId || '(missing)'} secretSet=${!!clientSecret}`); const cached = tokenCache.get(userId); if (cached && cached.expiresAt > Date.now() + 60_000) { + console.log(`[graph] OBO token from cache, expires in ${Math.round((cached.expiresAt - Date.now()) / 1000)}s`); return cached.accessToken; } + if (!tenantId || !oboClientId || !clientSecret) { + throw new Error(`OBO config incomplete: tenantId=${!!tenantId} oboClientId=${!!oboClientId} secret=${!!clientSecret}`); + } + const body = new URLSearchParams({ client_id: oboClientId, client_secret: clientSecret, @@ -35,6 +41,7 @@ async function getOboToken(userToken: string, userId: string): Promise { scope: 'https://graph.microsoft.com/Calendars.Read', }); + console.log(`[graph] OBO token exchange → https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`); const res = await fetch( `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body }, @@ -42,10 +49,12 @@ async function getOboToken(userToken: string, userId: string): Promise { if (!res.ok) { const detail = await res.text(); + console.error(`[graph] OBO exchange failed ${res.status}: ${detail}`); throw new Error(`OBO token exchange failed (${res.status}): ${detail}`); } const json = await res.json() as { access_token: string; expires_in: number }; + console.log(`[graph] OBO token obtained, expires_in=${json.expires_in}s`); const entry: OboTokenEntry = { accessToken: json.access_token, expiresAt: Date.now() + json.expires_in * 1000, @@ -66,6 +75,7 @@ export async function getCalendarEvents( date: string, ): Promise { + console.log(`[graph] getCalendarEvents userId=${userId} email=${userEmail} date=${date} tokenLen=${userToken.length}`); const graphToken = await getOboToken(userToken, userId); const start = `${date}T00:00:00`; @@ -87,8 +97,10 @@ export async function getCalendarEvents( if (!res.ok) { const detail = await res.text(); + console.error(`[graph] calendarView failed ${res.status}: ${detail}`); throw new Error(`Graph calendarView failed (${res.status}): ${detail}`); } + console.log(`[graph] calendarView OK`); type GraphEvent = { id: string; diff --git a/ka-note/server/src/routes/calendar.ts b/ka-note/server/src/routes/calendar.ts index 5574145..036eb3c 100644 --- a/ka-note/server/src/routes/calendar.ts +++ b/ka-note/server/src/routes/calendar.ts @@ -15,6 +15,13 @@ calendar.get('/events', async (c) => { const auth = c.get('auth'); const rawToken = c.req.header('Authorization')?.slice(7) ?? ''; + // OBO requires a real MSAL JWT — API keys and dev-bypass tokens don't work + const looksLikeJwt = rawToken.startsWith('eyJ') && rawToken.split('.').length === 3; + console.log(`[calendar] rawToken prefix="${rawToken.slice(0, 8)}..." looksLikeJwt=${looksLikeJwt} userId=${auth.userId}`); + if (!looksLikeJwt) { + return c.json({ error: 'graph_unavailable', detail: 'Calendar requires MSAL login (OBO not possible with API key or dev-bypass token)' }, 502); + } + try { const events = await getCalendarEvents(rawToken, auth.userId, auth.email, date); return c.json(events);