import jwt from 'jsonwebtoken'; import { OAuth2Client } from 'google-auth-library'; import env from '../../../config/env.js'; import * as db from '../../../db/googleAuth.js'; import { clearCachedUser } from '../../middleware/auth.js'; const googleClient = new OAuth2Client(env.googleClientId); /** * Registers the login route (Google SSO → JWT cookie session). * @param {import('express').Application} app - Express app * @param {string} [basePath=''] - Optional URL prefix for all routes in this module */ export function register(app, basePath = '') { /** * @openapi * /auth/login: * post: * tags: [Auth] * summary: Login with Google ID token * description: Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true`), and sets an HTTP-only `token` cookie. * security: [] * x-markdown: auth/login.md * requestBody: * required: true * content: * application/json: * schema: * type: object * required: [idToken] * properties: * idToken: * type: string * description: Google ID token from Google Identity Services * example: * idToken: "" * responses: * 200: * description: Login successful; `token` cookie set * content: * application/json: * schema: * type: object * properties: * message: * type: string * example: Login successful * user: * $ref: '#/components/schemas/User' * 400: * description: Missing token or Google client not configured * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Invalid Google token or account * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Email not on allowlist * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 500: * description: Server error * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ app.post(`${basePath}/auth/login`, async (req, res) => { try { const { idToken } = req.body; if (!idToken) { return res.status(400).json({ error: 'idToken is required' }); } if (!env.googleClientId) { return res.status(400).json({ error: 'GOOGLE_CLIENT_ID is not configured' }); } let ticket; try { ticket = await googleClient.verifyIdToken({ idToken, audience: env.googleClientId, }); } catch { return res.status(401).json({ error: 'Invalid Google token' }); } const payload = ticket.getPayload(); const email = payload?.email?.toLowerCase(); if (!email || !payload.email_verified) { return res.status(401).json({ error: 'Invalid Google account' }); } let allowed = await db.getAllowedEmail(email); if (!allowed && env.implicitCreate) { await db.addAllowedEmail(email); allowed = await db.getAllowedEmail(email); } if (!allowed) { console.warn(`Login rejected: email not authorized (${email})`); return res.status(403).json({ error: 'Email is not authorized' }); } await db.setProfile(allowed.id, { picture: payload.picture ?? null, sub: payload.sub ?? null, }); clearCachedUser(email); const user = { id: allowed.id, email: allowed.email, selectedCompanyId: allowed.selectedCompanyId, ...(payload.picture && { picture: payload.picture }), }; const token = jwt.sign(user, env.jwtSecret, { expiresIn: '1d' }); res.cookie('token', token, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 24 * 60 * 60 * 1000, }); res.status(200).json({ message: 'Login successful', user }); } catch (error) { console.error('Login failed:', error); res.status(500).json({ error: 'Login failed' }); } }); }