2026-04-07 21:58:25 -04:00
|
|
|
import 'dotenv/config';
|
|
|
|
|
import express from 'express';
|
|
|
|
|
import cors from 'cors';
|
|
|
|
|
import session from 'express-session';
|
|
|
|
|
import ConnectPgSimple from 'connect-pg-simple';
|
|
|
|
|
import pool from './db/pool.js';
|
|
|
|
|
import { errorHandler } from './middleware/errors.js';
|
|
|
|
|
|
2026-05-15 23:40:12 -04:00
|
|
|
// Routes
|
|
|
|
|
import authRouter from './routes/auth.js';
|
2026-04-07 21:58:25 -04:00
|
|
|
import assetsRouter from './routes/assets.js';
|
|
|
|
|
import projectsRouter from './routes/projects.js';
|
|
|
|
|
import binsRouter from './routes/bins.js';
|
|
|
|
|
import jobsRouter from './routes/jobs.js';
|
|
|
|
|
import captureRouter from './routes/capture.js';
|
2026-04-07 22:05:39 -04:00
|
|
|
import uploadRouter from './routes/upload.js';
|
|
|
|
|
import recordersRouter from './routes/recorders.js';
|
2026-04-18 13:42:09 -04:00
|
|
|
import settingsRouter from './routes/settings.js';
|
|
|
|
|
import amppRouter from './routes/ampp.js';
|
2026-05-18 21:25:36 -04:00
|
|
|
import usersRouter from './routes/users.js';
|
|
|
|
|
import groupsRouter from './routes/groups.js';
|
|
|
|
|
import tokensRouter from './routes/tokens.js';
|
2026-05-18 19:54:41 -04:00
|
|
|
import sequencesRouter from './routes/sequences.js';
|
2026-04-07 21:58:25 -04:00
|
|
|
|
2026-05-15 23:40:12 -04:00
|
|
|
const app = express();
|
2026-04-07 21:58:25 -04:00
|
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
|
|
2026-05-15 23:40:12 -04:00
|
|
|
// ── Middleware ────────────────────────────────────────────────────────────────
|
|
|
|
|
app.use(cors({ origin: true, credentials: true }));
|
2026-04-07 22:05:39 -04:00
|
|
|
app.use(express.json({ limit: '50mb' }));
|
2026-04-07 21:58:25 -04:00
|
|
|
|
|
|
|
|
const PgSession = ConnectPgSimple(session);
|
|
|
|
|
|
|
|
|
|
app.use(
|
|
|
|
|
session({
|
|
|
|
|
store: new PgSession({
|
|
|
|
|
pool,
|
|
|
|
|
tableName: 'sessions',
|
2026-05-15 23:40:12 -04:00
|
|
|
// Prune expired sessions every hour
|
|
|
|
|
pruneSessionInterval: 3600,
|
2026-04-07 21:58:25 -04:00
|
|
|
}),
|
2026-05-15 23:40:12 -04:00
|
|
|
secret: process.env.SESSION_SECRET || 'change-me-in-production',
|
|
|
|
|
resave: false,
|
2026-04-07 21:58:25 -04:00
|
|
|
saveUninitialized: false,
|
|
|
|
|
cookie: {
|
2026-05-15 23:40:12 -04:00
|
|
|
secure: process.env.NODE_ENV === 'production',
|
2026-04-07 21:58:25 -04:00
|
|
|
httpOnly: true,
|
2026-05-15 23:40:12 -04:00
|
|
|
maxAge: 1000 * 60 * 60 * 24, // 24 h
|
2026-04-07 21:58:25 -04:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-15 23:40:12 -04:00
|
|
|
// ── Health (no auth) ──────────────────────────────────────────────────────────
|
|
|
|
|
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
|
|
|
|
|
|
|
|
|
|
// ── API Routes ────────────────────────────────────────────────────────────────
|
|
|
|
|
// Auth routes are always open (login/logout don't require a session)
|
|
|
|
|
app.use('/api/v1/auth', authRouter);
|
2026-04-07 21:58:25 -04:00
|
|
|
|
2026-05-15 23:40:12 -04:00
|
|
|
// All other routes are gated by requireAuth (no-op unless AUTH_ENABLED=true)
|
|
|
|
|
app.use('/api/v1/assets', assetsRouter);
|
|
|
|
|
app.use('/api/v1/projects', projectsRouter);
|
|
|
|
|
app.use('/api/v1/bins', binsRouter);
|
|
|
|
|
app.use('/api/v1/jobs', jobsRouter);
|
|
|
|
|
app.use('/api/v1/capture', captureRouter);
|
|
|
|
|
app.use('/api/v1/upload', uploadRouter);
|
2026-04-07 22:05:39 -04:00
|
|
|
app.use('/api/v1/recorders', recordersRouter);
|
2026-05-15 23:40:12 -04:00
|
|
|
app.use('/api/v1/settings', settingsRouter);
|
|
|
|
|
app.use('/api/v1/ampp', amppRouter);
|
2026-05-18 21:25:36 -04:00
|
|
|
app.use('/api/v1/users', usersRouter);
|
|
|
|
|
app.use('/api/v1/groups', groupsRouter);
|
|
|
|
|
app.use('/api/v1/tokens', tokensRouter);
|
|
|
|
|
app.use('/api/v1/sequences', sequencesRouter);
|
2026-04-07 21:58:25 -04:00
|
|
|
|
2026-05-15 23:40:12 -04:00
|
|
|
// ── Error handler ─────────────────────────────────────────────────────────────
|
2026-04-07 21:58:25 -04:00
|
|
|
app.use(errorHandler);
|
|
|
|
|
|
2026-05-15 23:40:12 -04:00
|
|
|
// ── Start ────────────────────────────────────────────────────────────────────
|
2026-05-18 07:29:50 -04:00
|
|
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
|
import { dirname, join } from 'node:path';
|
|
|
|
|
|
|
|
|
|
const __dirnameMig = dirname(fileURLToPath(import.meta.url));
|
|
|
|
|
async function runMigrations() {
|
|
|
|
|
const dir = join(__dirnameMig, 'db', 'migrations');
|
|
|
|
|
let files = [];
|
|
|
|
|
try { files = readdirSync(dir).filter(f => f.endsWith('.sql')).sort(); } catch { return; }
|
|
|
|
|
for (const f of files) {
|
|
|
|
|
const sql = readFileSync(join(dir, f), 'utf8');
|
|
|
|
|
try {
|
|
|
|
|
await pool.query(sql);
|
|
|
|
|
console.log('[migration] applied ' + f);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('[migration] failed ' + f, err.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
await runMigrations();
|
|
|
|
|
|
2026-04-07 21:58:25 -04:00
|
|
|
app.listen(PORT, () => {
|
2026-05-15 23:40:12 -04:00
|
|
|
const authMode = process.env.AUTH_ENABLED === 'true' ? 'ENABLED' : 'DISABLED (set AUTH_ENABLED=true for production)';
|
2026-04-07 21:58:25 -04:00
|
|
|
console.log(`MAM API listening on port ${PORT}`);
|
2026-05-15 23:40:12 -04:00
|
|
|
console.log(`Authentication: ${authMode}`);
|
2026-04-07 21:58:25 -04:00
|
|
|
});
|