This commit is contained in:
seb
2026-10-29 16:27:02 +01:00
parent 005b48ff66
commit 676a40d4fe
6 changed files with 50 additions and 16 deletions

View File

@@ -235,7 +235,7 @@ _none_
- Returns: `200 { message: str }` - Returns: `200 { message: str }`
#### Current user — `GET /auth/me` #### Current user — `GET /auth/me`
_none_ _none_
- Returns: `200 { user: T32, googleClientId: str }` - Returns: `200 { user: T32, googleClientId: str, schemaVersion: int }`
#### Set selected company — `PUT /auth/selected-company` #### Set selected company — `PUT /auth/selected-company`
- Params: `body: { companyId: int }` - Params: `body: { companyId: int }`
- Returns: `200 { user: T32 }` - Returns: `200 { user: T32 }`

View File

@@ -6,7 +6,7 @@ Current user
Returns the authenticated user from the session cookie and the Google client ID. 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. `user.picture` is the stored profile image as a base64 string when present.
Unauthenticated calls still receive `googleClientId` with 401. Unauthenticated calls still receive `googleClientId` and `schemaVersion` with 401.
## Response `200` ## Response `200`
@@ -21,7 +21,8 @@ Authenticated
"selectedCompanyId": 1, "selectedCompanyId": 1,
"picture": "…" "picture": "…"
}, },
"googleClientId": "…" "googleClientId": "…",
"schemaVersion": 1
} }
``` ```
@@ -29,7 +30,7 @@ Authenticated
| Status | Description | | Status | Description |
|--------|-------------| |--------|-------------|
| 401 | Unauthorized — still includes `googleClientId` for the frontend | | 401 | Unauthorized — still includes `googleClientId` and `schemaVersion` for the frontend |
--- ---

View File

@@ -1,5 +1,16 @@
import env from '../../config/env.js'; import env from '../../config/env.js';
import { getSchemaVersion } from '../../db/store.js';
export function respondWithAuthenticatedUser(res, status, body) { export async function respondWithAuthenticatedUser(res, status, body) {
return res.status(status).json({ ...body, googleClientId: env.googleClientId }); let schemaVersion = null;
try {
schemaVersion = await getSchemaVersion();
} catch {
// ignore if database not accessible
}
return res.status(status).json({
...body,
googleClientId: env.googleClientId,
schemaVersion,
});
} }

View File

@@ -47,7 +47,7 @@ export function clearCachedCompanyUsers(companyId) {
} }
} }
function respondWithAuthPayload(res, req, status, body) { async function respondWithAuthPayload(res, req, status, body) {
if (req.path === '/api/auth/me') { if (req.path === '/api/auth/me') {
return respondWithAuthenticatedUser(res, status, body); return respondWithAuthenticatedUser(res, status, body);
} }
@@ -85,7 +85,7 @@ export const authMiddleware = async (req, res, next) => {
const isDocs = req.path.startsWith('/docs') || req.path.startsWith('/api/docs'); const isDocs = req.path.startsWith('/docs') || req.path.startsWith('/api/docs');
if (!token) { if (!token) {
if (isDocs || isOAuthAuthorize) return next(); if (isDocs || isOAuthAuthorize) return next();
return respondWithAuthPayload(res, req, 401, { error: 'Unauthorized' }); return await respondWithAuthPayload(res, req, 401, { error: 'Unauthorized' });
} }
try { try {
@@ -94,7 +94,7 @@ export const authMiddleware = async (req, res, next) => {
if (!Number.isInteger(authId) || authId <= 0) { if (!Number.isInteger(authId) || authId <= 0) {
if (req.cookies.token) clearTokenCookie(res); if (req.cookies.token) clearTokenCookie(res);
if (isDocs) return next(); if (isDocs) return next();
return respondWithAuthPayload(res, req, 401, { error: 'Invalid token' }); return await respondWithAuthPayload(res, req, 401, { error: 'Invalid token' });
} }
let user = getCachedUser(authId); let user = getCachedUser(authId);
@@ -111,7 +111,7 @@ export const authMiddleware = async (req, res, next) => {
clearCachedUser(authId); clearCachedUser(authId);
if (req.cookies.token) clearTokenCookie(res); if (req.cookies.token) clearTokenCookie(res);
if (isDocs) return next(); if (isDocs) return next();
return respondWithAuthPayload(res, req, 403, { error: 'User is no longer authorized' }); return await respondWithAuthPayload(res, req, 403, { error: 'User is no longer authorized' });
} }
req.user = { req.user = {
@@ -133,7 +133,7 @@ export const authMiddleware = async (req, res, next) => {
} catch { } catch {
if (req.cookies.token) clearTokenCookie(res); if (req.cookies.token) clearTokenCookie(res);
if (isDocs || isOAuthAuthorize) return next(); if (isDocs || isOAuthAuthorize) return next();
return respondWithAuthPayload(res, req, 401, { error: 'Invalid token' }); return await respondWithAuthPayload(res, req, 401, { error: 'Invalid token' });
} }
}; };

View File

@@ -17,7 +17,7 @@ export function register(app, basePath = '') {
* description: | * description: |
* Returns the authenticated user from the session cookie and the Google client ID. * 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. * `user.picture` is the stored profile image as a base64 string when present.
* Unauthenticated calls still receive `googleClientId` with 401. * Unauthenticated calls still receive `googleClientId` and `schemaVersion` with 401.
* security: * security:
* - cookieAuth: [] * - cookieAuth: []
* x-markdown: auth/me.md * x-markdown: auth/me.md
@@ -33,8 +33,11 @@ export function register(app, basePath = '') {
* $ref: '#/components/schemas/User' * $ref: '#/components/schemas/User'
* googleClientId: * googleClientId:
* type: string * type: string
* schemaVersion:
* type: integer
* description: Current database schema version
* 401: * 401:
* description: Unauthorized — still includes `googleClientId` for the frontend * description: Unauthorized — still includes `googleClientId` and `schemaVersion` for the frontend
* content: * content:
* application/json: * application/json:
* schema: * schema:
@@ -44,12 +47,15 @@ export function register(app, basePath = '') {
* type: string * type: string
* googleClientId: * googleClientId:
* type: string * type: string
* schemaVersion:
* type: integer
* description: Current database schema version
*/ */
app.get(`${basePath}/auth/me`, async (req, res) => { app.get(`${basePath}/auth/me`, async (req, res) => {
try { try {
const row = await db.getById(req.user.id); const row = await db.getById(req.user.id);
if (!row) { if (!row) {
return respondWithAuthenticatedUser(res, 403, { error: 'User is no longer authorized' }); return await respondWithAuthenticatedUser(res, 403, { error: 'User is no longer authorized' });
} }
const pictureBinary = toPictureBuffer(row.pictureBinary); const pictureBinary = toPictureBuffer(row.pictureBinary);
@@ -59,10 +65,10 @@ export function register(app, basePath = '') {
selectedCompanyId: row.selectedCompanyId, selectedCompanyId: row.selectedCompanyId,
...(pictureBinary && { picture: pictureBinary.toString('base64') }), ...(pictureBinary && { picture: pictureBinary.toString('base64') }),
}; };
respondWithAuthenticatedUser(res, 200, { user }); await respondWithAuthenticatedUser(res, 200, { user });
} catch (error) { } catch (error) {
console.error('Get current user failed:', error); console.error('Get current user failed:', error);
respondWithAuthenticatedUser(res, 500, { error: 'Failed to load user' }); await respondWithAuthenticatedUser(res, 500, { error: 'Failed to load user' });
} }
}); });
} }

View File

@@ -10,6 +10,7 @@ const schemaLog = createLogger('schema');
let provider; let provider;
let store; let store;
let connecting; let connecting;
let schemaVersion;
const STORE_NAMESPACES = [ const STORE_NAMESPACES = [
'companyuser', 'companyuser',
@@ -149,6 +150,7 @@ export async function getStore() {
provider = createProvider(); provider = createProvider();
try { try {
await provider.initialize((msg) => schemaLog.info(msg)); await provider.initialize((msg) => schemaLog.info(msg));
schemaVersion = await provider.getSchemaVersion();
} catch (err) { } catch (err) {
schemaLog.error('Database schema initialization failed:', err); schemaLog.error('Database schema initialization failed:', err);
throw err; throw err;
@@ -168,6 +170,19 @@ export async function getProvider() {
return provider; return provider;
} }
/**
* Returns the current database schema version.
* @returns {Promise<number>}
*/
export async function getSchemaVersion() {
if (schemaVersion != null) {
return schemaVersion;
}
const prov = await getProvider();
schemaVersion = await prov.getSchemaVersion();
return schemaVersion;
}
export async function ensureConnection() { export async function ensureConnection() {
await getStore(); await getStore();
} }
@@ -177,6 +192,7 @@ export async function closeStore() {
await provider.close(); await provider.close();
provider = undefined; provider = undefined;
store = undefined; store = undefined;
schemaVersion = undefined;
} }
} }