import { createOauthPendingState } from "../../../../db/mail"; import { MS_AUTH_BASE, MS_REDIRECT_URI, MS_OAUTH_SCOPES, generateCodeVerifier, computeCodeChallenge, generateStateId, } from "../../../../utils/ms-oauth"; /** * POST /api/mail/oauth/microsoft/init * * Step 1 of the Microsoft OAuth PKCE flow. * Generates a PKCE code_verifier + code_challenge, persists the state in * oauth_pending_states (TTL 10 min), and returns the authorization URL. * * The native app opens this URL via expo-web-browser (WebBrowser.openAuthSessionAsync). * After login + consent, Microsoft redirects to rebreak://auth/mail-oauth-callback?code=…&state=… * The app deep-link handler calls POST /api/mail/oauth/microsoft/callback with { code, state }. * * Body (optional): * email?: string — pre-fills login_hint + prompt logic * * Response: * 200: { authorizationUrl: string } * 401: not authenticated */ export default defineEventHandler(async (event) => { const user = await requireUser(event); const config = useRuntimeConfig(event); const clientId = config.msOauthClientId as string; if (!clientId) { throw createError({ statusCode: 500, data: { error: "MS_OAUTH_CLIENT_ID not configured" }, }); } const body = await readBody(event).catch(() => ({})) as { email?: string }; const hintEmail = body?.email?.trim() || null; // ── PKCE ───────────────────────────────────────────────────────────────── const codeVerifier = generateCodeVerifier(); const codeChallenge = computeCodeChallenge(codeVerifier); const stateId = generateStateId(); // ── Persist state (TTL enforced at read time in callback) ───────────────── // Clean up stale entries for this user (older than 10 min) before inserting. await createOauthPendingState({ stateId, userId: user.id, codeVerifier, email: hintEmail, }); // ── Build authorization URL ─────────────────────────────────────────────── const params = new URLSearchParams({ client_id: clientId, response_type: "code", redirect_uri: MS_REDIRECT_URI, response_mode: "query", scope: MS_OAUTH_SCOPES, state: stateId, code_challenge: codeChallenge, code_challenge_method: "S256", // prompt=consent: always show consent screen so user sees what scopes are requested. // Alternative: 'select_account' for re-connect flows (less friction but skips // scope confirmation). Using 'consent' is the safer DSGVO-aligned default. prompt: "consent", }); if (hintEmail) { params.set("login_hint", hintEmail); } const authorizationUrl = `${MS_AUTH_BASE}?${params.toString()}`; return { authorizationUrl }; });