import { createOauthPendingState } from "../../../../db/mail"; import { GOOGLE_AUTH_BASE, GOOGLE_REDIRECT_URI, GOOGLE_OAUTH_SCOPES, generateCodeVerifier, computeCodeChallenge, generateStateId, } from "../../../../utils/google-oauth"; /** * POST /api/mail/oauth/google/init * * Step 1 des Google OAuth PKCE-Flows. * Generiert PKCE code_verifier + code_challenge, persistiert State in * oauth_pending_states (TTL 10 min), gibt Authorization-URL zurück. * * Die native App öffnet die URL via expo-web-browser (WebBrowser.openAuthSessionAsync). * Nach Login + Consent leitet Google weiter auf rebreak://auth/mail-oauth-callback?code=…&state=… * Der Deep-Link-Handler der App ruft POST /api/mail/oauth/google/callback mit { code, state }. * * Google-Spezifik: * prompt=consent — PFLICHT damit Google refresh_token zurückgibt. Ohne das: * kein refresh_token beim Exchange → Connection lebt nur 1h. * access_type=offline — PFLICHT für refresh_token (Google-spezifisch, MS braucht das nicht). * * Body (optional): * email?: string — pre-fills login_hint * * Response: * 200: { authorizationUrl: string } * 401: not authenticated * 500: GOOGLE_OAUTH_CLIENT_ID not configured */ export default defineEventHandler(async (event) => { const user = await requireUser(event); const config = useRuntimeConfig(event); const clientId = config.googleOauthClientId as string; if (!clientId) { throw createError({ statusCode: 500, data: { error: "GOOGLE_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) ────────────────── await createOauthPendingState({ stateId, userId: user.id, codeVerifier, email: hintEmail, }); // ── Build authorization URL ───────────────────────────────────────────────── const params = new URLSearchParams({ client_id: clientId, response_type: "code", redirect_uri: GOOGLE_REDIRECT_URI, scope: GOOGLE_OAUTH_SCOPES, state: stateId, code_challenge: codeChallenge, code_challenge_method: "S256", // access_type=offline: PFLICHT damit Google ein refresh_token ausstellt. // Bei access_type=online (default) gibt es keinen refresh_token. access_type: "offline", // prompt=consent: PFLICHT damit Google den refresh_token zurückgibt // (auch wenn der User die App schon einmal autorisiert hat). // Ohne das: bei bestehender Berechtigung kommt kein refresh_token. prompt: "consent", }); if (hintEmail) { params.set("login_hint", hintEmail); } const authorizationUrl = `${GOOGLE_AUTH_BASE}?${params.toString()}`; return { authorizationUrl }; });