This commit is contained in:
seb
2026-06-23 11:04:07 +02:00
commit 667f498573
19 changed files with 4299 additions and 0 deletions

136
src/api/index.js Normal file
View File

@@ -0,0 +1,136 @@
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import { fileURLToPath, pathToFileURL } from 'url';
import express from 'express';
import cookieParser from 'cookie-parser';
import { loggerMiddleware } from './middleware/logger.js';
import env from '../config/env.js';
import { authMiddleware } from './middleware/auth.js';
import { ensureConnection } from '../db/client.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function loadRoutes(app, basePath = '') {
const routesDir = path.join(__dirname, 'routes');
async function walk(dir) {
for (const file of fs.readdirSync(dir)) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
await walk(fullPath);
} else if (file.endsWith('.js')) {
const routeModule = await import(pathToFileURL(fullPath).href);
if (routeModule.register) {
routeModule.register(app, basePath);
}
}
}
}
await walk(routesDir);
}
let server;
let starting;
let startedAt;
function corsMiddleware(req, res, next) {
const origin = req.headers.origin;
if (origin && env.corsOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
}
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
}
async function createApi() {
const app = express();
app.use(corsMiddleware);
app.use(express.json());
app.use(cookieParser());
app.use(loggerMiddleware);
app.use(authMiddleware);
if (startedAt) {
app.use((req, res, next) => {
res.setHeader('X-Server-Started-At', startedAt);
next();
});
}
await loadRoutes(app, '/api');
return app;
}
export async function startApi() {
if (server) {
return server;
}
if (starting) {
return starting;
}
starting = (async () => {
await ensureConnection();
startedAt = new Date().toISOString();
const app = await createApi();
await new Promise((resolve, reject) => {
const instance = app.listen(env.port);
instance.once('listening', () => {
server = instance;
console.log(`API listening on port ${env.port}`);
resolve();
});
instance.once('error', (err) => {
if (err.code === 'EADDRINUSE') {
let detail = '';
try {
const pids = execSync(`lsof -ti :${env.port}`, { encoding: 'utf8' }).trim();
if (pids) detail = ` (PID ${pids.replace(/\n/g, ', ')})`;
} catch {
// lsof unavailable or no match
}
console.error(`Port ${env.port} is already in use${detail}. Check with: lsof -i :${env.port}`);
} else {
console.error('Server error:', err.message);
}
reject(err);
});
});
return server;
})();
try {
return await starting;
} finally {
starting = undefined;
}
}
export async function stopApi() {
if (!server) return;
await new Promise((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
server = undefined;
}