// lambdas/auth/index.js // Public (unauthenticated) endpoints: // GET /auth/me — returns user profile from Google token (client calls after login) // GET /auth/config — returns Google Client ID so the frontend can init Google Sign-In // POST /auth/logout — clears server-side session record const { OAuth2Client } = require("google-auth-library"); const { DynamoDBDocumentClient, DeleteCommand } = require("@aws-sdk/lib-dynamodb"); const { DynamoDBClient } = require("@aws-sdk/client-dynamodb"); const { ok, unauthorized, serverError, CORS_HEADERS, parseBody } = require("@ledgerflow/shared"); const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID; const SESSIONS_TABLE = process.env.SESSIONS_TABLE || "ledgerflow-sessions"; const googleClient = new OAuth2Client(GOOGLE_CLIENT_ID); const dynamo = DynamoDBDocumentClient.from( new DynamoDBClient({ region: process.env.AWS_REGION || "us-east-1" }) ); exports.handler = async (event) => { // CORS preflight if (event.requestContext?.http?.method === "OPTIONS") { return { statusCode: 200, headers: CORS_HEADERS, body: "" }; } const method = event.requestContext?.http?.method || event.httpMethod; const path = event.rawPath || event.path || ""; try { // ── GET /auth/config ──────────────────────────────────────────────────── // Returns public config the frontend needs to initialize Google Sign-In if (method === "GET" && path.endsWith("/config")) { return ok({ googleClientId: GOOGLE_CLIENT_ID, // The frontend redirects here after Google login // Using Google's newer Identity Services (one-tap / button flow) // No redirect URI needed — token is returned directly to the JS callback }); } // ── GET /auth/me ──────────────────────────────────────────────────────── // Validates Bearer token and returns user profile // Called immediately after Google Sign-In succeeds on the frontend if (method === "GET" && path.endsWith("/me")) { const authHeader = event.headers?.authorization || event.headers?.Authorization || ""; if (!authHeader.startsWith("Bearer ")) return unauthorized("Missing token"); const token = authHeader.slice(7); const ticket = await googleClient.verifyIdToken({ idToken: token, audience: GOOGLE_CLIENT_ID, }); const payload = ticket.getPayload(); if (!payload) return unauthorized("Invalid token"); return ok({ userId: payload.sub, email: payload.email, name: payload.name, picture: payload.picture, domain: payload.hd || null, // Google Workspace hosted domain }); } // ── POST /auth/logout ─────────────────────────────────────────────────── // Removes server-side session (best-effort) if (method === "POST" && path.endsWith("/logout")) { const authHeader = event.headers?.authorization || event.headers?.Authorization || ""; if (authHeader.startsWith("Bearer ")) { const token = authHeader.slice(7); try { const ticket = await googleClient.verifyIdToken({ idToken: token, audience: GOOGLE_CLIENT_ID }); const payload = ticket.getPayload(); if (payload?.sub) { await dynamo.send(new DeleteCommand({ TableName: SESSIONS_TABLE, Key: { userId: payload.sub } })); } } catch { // Best-effort; token may already be expired } } return ok({ message: "Logged out" }); } return { statusCode: 404, headers: CORS_HEADERS, body: JSON.stringify({ error: "Not Found" }) }; } catch (err) { console.error("[AUTH]", err); return serverError("Auth error", err); } };