Files
dashback/src/api/routes/auth/login.js
seb c78237a1dd Persist Google picture and sub on login via setProfile.
Store profile fields from the verified ID token and read picture from the DB on authenticated requests.
2026-08-05 11:04:44 +02:00

140 lines
4.9 KiB
JavaScript

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: "<google-id-token>"
* 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' });
}
});
}