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