// Static file serving + authenticated recording/transcript downloads. // handleGet() is the fallback for any GET that didn't match an API route. const fs = require('fs'); const path = require('path'); const R = require('./repos'); const { json } = require('./lib'); const { currentUser } = require('./session'); const { PUBLIC_DIR, REC_DIR, TRANS_DIR } = require('./config'); const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.svg': 'image/svg+xml', '.ico': 'image/x-icon' }; function serveStatic(req, res) { let p = req.url.split('?')[0]; if (p === '/') p = '/index.html'; if (p === '/home') p = '/home.html'; // Console was replaced by Dashboard; keep the old path working. if (p === '/console' || p === '/dashboard') p = '/dashboard.html'; if (p === '/share') p = '/share.html'; if (p === '/connect') p = '/connect.html'; const fp = path.join(PUBLIC_DIR, path.normalize(p)); if (!fp.startsWith(PUBLIC_DIR)) return json(res, 403, { error: 'forbidden' }); fs.readFile(fp, (err, data) => { if (err) return json(res, 404, { error: 'not found' }); const ct = MIME[path.extname(fp)] || 'application/octet-stream'; res.writeHead(200, { 'Content-Type': ct, 'Cache-Control': 'no-cache' }); res.end(data); }); } // GET fallback: authenticated transcript/recording downloads, else static files. function handleGet(req, res) { const pathOnly = req.url.split('?')[0]; if (pathOnly.startsWith('/transcripts/')) { const u = currentUser(req); if (!u) return json(res, 401, { error: 'unauthorized' }); const name = path.basename(decodeURIComponent(pathOnly)); const sid = name.replace(/\.txt$/i, ''); const row = R.sessionsLog.byIdInTenant(sid, u.team_id); if (!row || !row.transcript) return json(res, 404, { error: 'not found' }); const fp = path.join(TRANS_DIR, row.transcript); if (!fp.startsWith(TRANS_DIR)) return json(res, 403, { error: 'forbidden' }); return fs.stat(fp, (err, st) => { if (err) return json(res, 404, { error: 'not found' }); res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': st.size, 'Content-Disposition': 'attachment; filename="transcript-' + sid + '.txt"', 'Cache-Control': 'no-store' }); const rs = fs.createReadStream(fp); rs.on('error', () => { try { res.destroy(); } catch (e) {} }); rs.pipe(res); }); } if (pathOnly.startsWith('/recordings/')) { const u = currentUser(req); if (!u) return json(res, 401, { error: 'unauthorized' }); const name = path.basename(decodeURIComponent(pathOnly)); const sid = name.replace(/\.(webm|mp4)$/i, ''); const row = R.sessionsLog.byIdInTenant(sid, u.team_id); if (!row || !row.recording) return json(res, 404, { error: 'not found' }); const fp = path.join(REC_DIR, row.recording); if (!fp.startsWith(REC_DIR)) return json(res, 403, { error: 'forbidden' }); const ext = path.extname(row.recording).toLowerCase() === '.mp4' ? 'mp4' : 'webm'; const ctype = ext === 'mp4' ? 'video/mp4' : 'video/webm'; return fs.stat(fp, (err, st) => { if (err) return json(res, 404, { error: 'not found' }); res.writeHead(200, { 'Content-Type': ctype, 'Content-Length': st.size, 'Content-Disposition': 'attachment; filename="session-' + sid + '.' + ext + '"', 'Cache-Control': 'no-store', 'Accept-Ranges': 'bytes' }); const rs = fs.createReadStream(fp); rs.on('error', () => { try { res.destroy(); } catch (e) {} }); rs.pipe(res); }); } return serveStatic(req, res); } module.exports = { handleGet, serveStatic };