Store Google avatar bytes and slim the session JWT to id only.
Download profile images on login into picturebinary, return them as base64 from /auth/me, and resolve sessions via getById.
This commit is contained in:
@@ -3,6 +3,7 @@ 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';
|
||||
import { downloadPicture, toPictureBuffer } from '../../../util/pictureBinary.js';
|
||||
|
||||
const googleClient = new OAuth2Client(env.googleClientId);
|
||||
|
||||
@@ -18,7 +19,10 @@ export function register(app, basePath = '') {
|
||||
* 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.
|
||||
* description: |
|
||||
* Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true`),
|
||||
* stores Google `sub` and profile image bytes, and sets an HTTP-only `token` cookie.
|
||||
* `user.picture` is the profile image as base64 (same as GET /auth/me).
|
||||
* security: []
|
||||
* x-markdown: auth/login.md
|
||||
* requestBody:
|
||||
@@ -109,19 +113,38 @@ export function register(app, basePath = '') {
|
||||
return res.status(403).json({ error: 'Email is not authorized' });
|
||||
}
|
||||
|
||||
const pictureUrl = typeof payload.picture === 'string' ? payload.picture : null;
|
||||
const sub = typeof payload.sub === 'string' ? payload.sub : null;
|
||||
const existingBinary = toPictureBuffer(allowed.pictureBinary);
|
||||
let pictureBinary = null;
|
||||
|
||||
if (pictureUrl) {
|
||||
if (pictureUrl === allowed.picture && existingBinary) {
|
||||
pictureBinary = existingBinary;
|
||||
} else {
|
||||
try {
|
||||
pictureBinary = await downloadPicture(pictureUrl);
|
||||
} catch (err) {
|
||||
console.warn(`Profile picture download failed (${email}):`, err.message);
|
||||
pictureBinary = pictureUrl === allowed.picture ? existingBinary : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await db.setProfile(allowed.id, {
|
||||
picture: payload.picture ?? null,
|
||||
sub: payload.sub ?? null,
|
||||
picture: pictureUrl,
|
||||
pictureBinary,
|
||||
sub,
|
||||
});
|
||||
clearCachedUser(email);
|
||||
clearCachedUser(allowed.id);
|
||||
|
||||
const user = {
|
||||
id: allowed.id,
|
||||
email: allowed.email,
|
||||
selectedCompanyId: allowed.selectedCompanyId,
|
||||
...(payload.picture && { picture: payload.picture }),
|
||||
...(pictureBinary && { picture: pictureBinary.toString('base64') }),
|
||||
};
|
||||
const token = jwt.sign(user, env.jwtSecret, { expiresIn: '1d' });
|
||||
const token = jwt.sign({ id: user.id }, env.jwtSecret, { expiresIn: '1d' });
|
||||
|
||||
res.cookie('token', token, {
|
||||
httpOnly: true,
|
||||
|
||||
@@ -34,8 +34,9 @@ export function register(app, basePath = '') {
|
||||
if (token) {
|
||||
try {
|
||||
const decoded = jwt.verify(token, env.jwtSecret, { ignoreExpiration: true });
|
||||
if (decoded?.email) {
|
||||
clearCachedUser(decoded.email);
|
||||
const authId = Number(decoded?.id);
|
||||
if (Number.isInteger(authId) && authId > 0) {
|
||||
clearCachedUser(authId);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during cache clearing on logout
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { meJson } from '../../auth/meJson.js';
|
||||
import * as db from '../../../db/googleAuth.js';
|
||||
import { toPictureBuffer } from '../../../util/pictureBinary.js';
|
||||
|
||||
/**
|
||||
* Registers the current-user route (requires prior auth middleware populating `req.user`).
|
||||
@@ -12,7 +14,10 @@ export function register(app, basePath = '') {
|
||||
* get:
|
||||
* tags: [Auth]
|
||||
* summary: Current user
|
||||
* description: Returns the authenticated user from the session cookie and the Google client ID. Unauthenticated calls still receive `googleClientId` with 401.
|
||||
* description: |
|
||||
* Returns the authenticated user from the session cookie and the Google client ID.
|
||||
* `user.picture` is the stored profile image as a base64 string when present.
|
||||
* Unauthenticated calls still receive `googleClientId` with 401.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: auth/me.md
|
||||
@@ -40,9 +45,24 @@ export function register(app, basePath = '') {
|
||||
* googleClientId:
|
||||
* type: string
|
||||
*/
|
||||
app.get(`${basePath}/auth/me`, (req, res) => {
|
||||
const { id, email, selectedCompanyId, picture } = req.user;
|
||||
const user = { id, email, selectedCompanyId, ...(picture && { picture }) };
|
||||
meJson(res, 200, { user });
|
||||
app.get(`${basePath}/auth/me`, async (req, res) => {
|
||||
try {
|
||||
const row = await db.getById(req.user.id);
|
||||
if (!row) {
|
||||
return meJson(res, 403, { error: 'User is no longer authorized' });
|
||||
}
|
||||
|
||||
const pictureBinary = toPictureBuffer(row.pictureBinary);
|
||||
const user = {
|
||||
id: row.id,
|
||||
email: row.email,
|
||||
selectedCompanyId: row.selectedCompanyId,
|
||||
...(pictureBinary && { picture: pictureBinary.toString('base64') }),
|
||||
};
|
||||
meJson(res, 200, { user });
|
||||
} catch (error) {
|
||||
console.error('Get current user failed:', error);
|
||||
meJson(res, 500, { error: 'Failed to load user' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,14 +67,13 @@ export function register(app, basePath = '') {
|
||||
|
||||
try {
|
||||
await db.setSelectedCompany(req.user.id, companyId);
|
||||
clearCachedUser(req.user.email);
|
||||
clearCachedUser(req.user.id);
|
||||
|
||||
const updated = await db.getAllowedEmail(req.user.email);
|
||||
const user = {
|
||||
id: updated.id,
|
||||
email: updated.email,
|
||||
selectedCompanyId: updated.selectedCompanyId,
|
||||
...(req.user.picture && { picture: req.user.picture }),
|
||||
};
|
||||
|
||||
res.status(200).json({ user });
|
||||
|
||||
Reference in New Issue
Block a user