ledgerflow-backend/lambdas/auth/index.js
Adam Moussa 59127d5ab8 Initial commit — LedgerFlow backend
Lambda-based serverless backend with Google SSO, purchase orders,
invoices, and X12 810 EDI generation for Amazon Payee Central.
Includes bill-to/ship-to address support from Coupa purchase-orders table.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 18:19:38 -04:00

90 lines
4 KiB
JavaScript

// 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);
}
};