You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

74 lines
2.1 KiB
TypeScript

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;
}