import express from 'express'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import QRCode from 'qrcode'; import type { AuthManager } from './auth.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export function createApp(auth: AuthManager, qrUrlRef: { url: string }): express.Express { const app = express(); // Security headers app.use(auth.cspMiddleware()); // Parse JSON bodies app.use(express.json()); // QR code endpoint — generates SVG for the current access URL app.get('/api/qr.svg', async (_req, res) => { try { const svg = await QRCode.toString(qrUrlRef.url, { type: 'svg' }); res.setHeader('Content-Type', 'image/svg+xml'); res.send(svg); } catch { res.status(500).send('QR generation failed'); } }); // Access URL endpoint — returns the current access URL (tunnel or local) app.get('/api/access-url', (_req, res) => { res.json({ url: qrUrlRef.url }); }); // Auth status endpoint app.get('/api/auth/status', (_req, res) => { res.json({ authRequired: !auth.isAuthDisabled() }); }); // Auth endpoint app.post('/api/auth/login', auth.loginHandler()); // Token verification endpoint app.get('/api/auth/verify', (req, res) => { if (auth.isAuthDisabled()) { res.json({ valid: true }); return; } const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) { res.status(401).json({ valid: false }); return; } const token = authHeader.slice(7); const valid = auth.verifyToken(token); res.json({ valid }); }); // Static files — no-cache so changes are always picked up const publicDir = join(__dirname, '..', '..', 'public'); app.use(express.static(publicDir, { setHeaders: (res) => { res.setHeader('Cache-Control', 'no-store'); }, })); // SPA fallback — serve index.html for non-API, non-file routes app.get('/{*splat}', (_req, res) => { res.sendFile(join(publicDir, 'index.html')); }); return app; }