This commit is contained in:
seb
2026-10-09 10:43:56 +02:00
parent 011d84c3c1
commit 728b3ec172
3 changed files with 35 additions and 35 deletions

View File

@@ -22,16 +22,16 @@ const apiLog = createLogger('api');
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
async function loadRoutes(app, basePath = '') { async function registerRoutes(app, basePath = '') {
const routesDir = path.join(__dirname, 'routes'); const routesDir = path.join(__dirname, 'routes');
async function walk(dir) { async function registerRouteFilesRecursively(dir) {
for (const file of fs.readdirSync(dir)) { for (const file of fs.readdirSync(dir)) {
const fullPath = path.join(dir, file); const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath); const stat = fs.statSync(fullPath);
if (stat.isDirectory()) { if (stat.isDirectory()) {
await walk(fullPath); await registerRouteFilesRecursively(fullPath);
} else if (file.endsWith('.js')) { } else if (file.endsWith('.js')) {
const routeModule = await import(pathToFileURL(fullPath).href); const routeModule = await import(pathToFileURL(fullPath).href);
if (routeModule.register) { if (routeModule.register) {
@@ -41,17 +41,17 @@ async function loadRoutes(app, basePath = '') {
} }
} }
await walk(routesDir); await registerRouteFilesRecursively(routesDir);
} }
let server; let server;
let httpsServer; let httpsServer;
let starting; let startupPromise;
let startedAt; let serverStartedAt;
const PRIVATE_HOST_REGEX = /^(127\.0\.0\.1|localhost|192\.168\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})$/; const LOCAL_PRIVATE_HOST_PATTERN = /^(127\.0\.0\.1|localhost|192\.168\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})$/;
function isAllowedOrigin(origin) { function isAllowedCorsOrigin(origin) {
if (!origin) return false; if (!origin) return false;
if (env.corsOrigins.includes(origin)) return true; if (env.corsOrigins.includes(origin)) return true;
try { try {
@@ -59,7 +59,7 @@ function isAllowedOrigin(origin) {
if (u.hostname === 'quixpos.com' || u.hostname.endsWith('.quixpos.com')) { if (u.hostname === 'quixpos.com' || u.hostname.endsWith('.quixpos.com')) {
return true; return true;
} }
if (PRIVATE_HOST_REGEX.test(u.hostname)) { if (LOCAL_PRIVATE_HOST_PATTERN.test(u.hostname)) {
return true; return true;
} }
} catch { } catch {
@@ -101,7 +101,7 @@ function corsMiddleware(req, res, next) {
return next(); return next();
} }
if (origin && isAllowedOrigin(origin)) { if (origin && isAllowedCorsOrigin(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin'); res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Allow-Credentials', 'true');
@@ -120,7 +120,7 @@ function corsMiddleware(req, res, next) {
next(); next();
} }
async function createApi() { async function buildApiApp() {
const app = express(); const app = express();
app.use((req, res, next) => { app.use((req, res, next) => {
@@ -136,14 +136,14 @@ async function createApi() {
app.use(loggerMiddleware); app.use(loggerMiddleware);
app.use(authMiddleware); app.use(authMiddleware);
if (startedAt) { if (serverStartedAt) {
app.use((req, res, next) => { app.use((req, res, next) => {
res.setHeader('X-Server-Started-At', startedAt); res.setHeader('X-Server-Started-At', serverStartedAt);
next(); next();
}); });
} }
await loadRoutes(app, '/api'); await registerRoutes(app, '/api');
app.use('/v1', v1Router); app.use('/v1', v1Router);
app.use('/api/v1', v1Router); app.use('/api/v1', v1Router);
@@ -166,7 +166,7 @@ async function createApi() {
return app; return app;
} }
function listenServer(instance, port, protocol) { function listenOnPort(instance, port, protocol) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
instance.once('listening', () => { instance.once('listening', () => {
resolve(); resolve();
@@ -196,18 +196,18 @@ export async function startApi() {
if (server || httpsServer) { if (server || httpsServer) {
return server; return server;
} }
if (starting) { if (startupPromise) {
return starting; return startupPromise;
} }
starting = (async () => { startupPromise = (async () => {
await ensureConnection(); await ensureConnection();
startedAt = new Date().toISOString(); serverStartedAt = new Date().toISOString();
const app = await createApi(); const app = await buildApiApp();
const httpInstance = http.createServer(app); const httpInstance = http.createServer(app);
await listenServer(httpInstance, env.port, 'HTTP'); await listenOnPort(httpInstance, env.port, 'HTTP');
server = httpInstance; server = httpInstance;
apiLog.info(`API listening on port ${env.port} (HTTP)`); apiLog.info(`API listening on port ${env.port} (HTTP)`);
@@ -215,7 +215,7 @@ export async function startApi() {
try { try {
const tls = await getOrCreateCert(); const tls = await getOrCreateCert();
const httpsInstance = https.createServer(tls, app); const httpsInstance = https.createServer(tls, app);
await listenServer(httpsInstance, env.httpsPort, 'HTTPS'); await listenOnPort(httpsInstance, env.httpsPort, 'HTTPS');
httpsServer = httpsInstance; httpsServer = httpsInstance;
apiLog.info(`API listening on port ${env.httpsPort} (HTTPS self-signed)`); apiLog.info(`API listening on port ${env.httpsPort} (HTTPS self-signed)`);
} catch (err) { } catch (err) {
@@ -227,9 +227,9 @@ export async function startApi() {
})(); })();
try { try {
return await starting; return await startupPromise;
} finally { } finally {
starting = undefined; startupPromise = undefined;
} }
} }

View File

@@ -1,7 +1,7 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import env from '../../config/env.js'; import env from '../../config/env.js';
import * as db from '../../db/googleAuth.js'; import * as db from '../../db/googleAuth.js';
import { meJson } from '../auth/meJson.js'; import { respondWithAuthenticatedUser } from '../auth/meJson.js';
const userCache = new Map(); const userCache = new Map();
const USER_CACHE_TTL = 10 * 60 * 1000; const USER_CACHE_TTL = 10 * 60 * 1000;
@@ -39,9 +39,9 @@ export function clearCachedUser(authId) {
userCache.delete(authId); userCache.delete(authId);
} }
function authMeJson(res, req, status, body) { function respondWithAuthPayload(res, req, status, body) {
if (req.path === '/api/auth/me') { if (req.path === '/api/auth/me') {
return meJson(res, status, body); return respondWithAuthenticatedUser(res, status, body);
} }
return res.status(status).json(body); return res.status(status).json(body);
} }
@@ -71,7 +71,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) return next(); if (isDocs) return next();
return authMeJson(res, req, 401, { error: 'Unauthorized' }); return respondWithAuthPayload(res, req, 401, { error: 'Unauthorized' });
} }
try { try {
@@ -80,7 +80,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 authMeJson(res, req, 401, { error: 'Invalid token' }); return respondWithAuthPayload(res, req, 401, { error: 'Invalid token' });
} }
let user = getCachedUser(authId); let user = getCachedUser(authId);
@@ -97,7 +97,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 authMeJson(res, req, 403, { error: 'User is no longer authorized' }); return respondWithAuthPayload(res, req, 403, { error: 'User is no longer authorized' });
} }
req.user = { req.user = {
@@ -119,7 +119,7 @@ export const authMiddleware = async (req, res, next) => {
} catch { } catch {
if (req.cookies.token) clearTokenCookie(res); if (req.cookies.token) clearTokenCookie(res);
if (isDocs) return next(); if (isDocs) return next();
return authMeJson(res, req, 401, { error: 'Invalid token' }); return respondWithAuthPayload(res, req, 401, { error: 'Invalid token' });
} }
}; };

View File

@@ -1,4 +1,4 @@
import { meJson } from '../../auth/meJson.js'; import { respondWithAuthenticatedUser } from '../../auth/meJson.js';
import * as db from '../../../db/googleAuth.js'; import * as db from '../../../db/googleAuth.js';
import { toPictureBuffer } from '../../../util/pictureBinary.js'; import { toPictureBuffer } from '../../../util/pictureBinary.js';
@@ -49,7 +49,7 @@ export function register(app, basePath = '') {
try { try {
const row = await db.getById(req.user.id); const row = await db.getById(req.user.id);
if (!row) { if (!row) {
return meJson(res, 403, { error: 'User is no longer authorized' }); return respondWithAuthenticatedUser(res, 403, { error: 'User is no longer authorized' });
} }
const pictureBinary = toPictureBuffer(row.pictureBinary); const pictureBinary = toPictureBuffer(row.pictureBinary);
@@ -59,10 +59,10 @@ export function register(app, basePath = '') {
selectedCompanyId: row.selectedCompanyId, selectedCompanyId: row.selectedCompanyId,
...(pictureBinary && { picture: pictureBinary.toString('base64') }), ...(pictureBinary && { picture: pictureBinary.toString('base64') }),
}; };
meJson(res, 200, { user }); respondWithAuthenticatedUser(res, 200, { user });
} catch (error) { } catch (error) {
console.error('Get current user failed:', error); console.error('Get current user failed:', error);
meJson(res, 500, { error: 'Failed to load user' }); respondWithAuthenticatedUser(res, 500, { error: 'Failed to load user' });
} }
}); });
} }