Store profile fields from the verified ID token and read picture from the DB on authenticated requests.
140 lines
4.9 KiB
JavaScript
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' });
|
|
}
|
|
});
|
|
}
|