import { callAgent } from './agentClient.js'; import { registerBrowserSocketHandlers } from './socketHandlers.js'; import 'dotenv/config'; import express from 'express'; import cors from 'cors'; import session from 'express-session'; import { OAuth2Client } from 'google-auth-library'; import { createServer } from 'node:http'; import { Server } from 'socket.io'; import path from 'path'; import { fileURLToPath } from 'url'; const app = express(); const http = createServer(app); const front = process.env.FRONTEND_URL || 'http://localhost:5173'; const emails = new Set((process.env.ALLOWED_EMAILS || '').split(',').filter(Boolean)); const oauth = new OAuth2Client(process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, `${front}/auth/callback`); const __dirname = path.dirname(fileURLToPath(import.meta.url)); const frontendDistPath = path.join(__dirname, '../frontend/dist'); const frontendIndexPath = path.join(frontendDistPath, 'index.html'); if (process.env.NODE_ENV === 'production') { app.set('trust proxy', 1); } const sessionSecret = process.env.SESSION_SECRET; if (!sessionSecret || (sessionSecret === 'CHANGE_ME' && process.env.NODE_ENV !== 'production')) { throw new Error('A valid SESSION_SECRET must be set.'); } const sm = session({ secret: sessionSecret, resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' }, }); const io = new Server(http, { cors: { origin: front, credentials: true } }); app.use(cors({ origin: front, credentials: true })); app.use(sm); app.use(express.static(frontendDistPath)); io.use((s, n) => sm(s.request, {}, n)); app.get('/health', (_, r) => r.json({ status: 'ok' })); app.get('/auth/google', (q, r) => r.redirect(oauth.generateAuthUrl({ scope: ['openid', 'email', 'profile'], prompt: 'consent' }))); app.get('/auth/callback', async (req, res, next) => { try { let { tokens } = await oauth.getToken(req.query.code); let p = (await oauth.verifyIdToken({ idToken: tokens.id_token, audience: process.env.GOOGLE_CLIENT_ID })).getPayload(); if (!p?.email || !emails.has(p.email)) return res.status(403).send('Not authorised'); req.session.user = { email: p.email, name: p.name || p.email }; res.redirect('/'); } catch (e) { next(e); } }); app.get('/auth/me', (q, r) => (q.session.user ? r.json(q.session.user) : r.status(401).end())); app.post('/auth/logout', (q, r) => q.session.destroy(() => r.json({ ok: true }))); const ensureAuthenticated = (req, res, next) => { if (req.session.user) { return next(); } res.status(401).end(); }; const consoleApiRouter = require('./routes/consoleApi.js'); app.use('/api/console', ensureAuthenticated, consoleApiRouter); io.on('connection', (s) => { const u = s.request.session?.user; if (!u) return s.disconnect(); registerBrowserSocketHandlers(s, { callAgentFn: callAgent }); }); app.get('*' , (req, res) => { if (req.method !== 'GET') { return res.status(404).end(); } const isApiRoute = /^\/(auth|socket\.io|health|api)(\/.*|\/?)$/.test(req.path); if (isApiRoute) { return res.status(404).end(); } if (req.accepts('html')) { res.sendFile(frontendIndexPath, (err) => { if (err && !res.headersSent) { res.status(404).end(); } }); } else { if (!res.headersSent) { res.status(404).end(); } } }); // Central Error Handler app.use((err, req, res, next) => { // TODO: Integrate with an external error tracking service like Sentry console.error(`[Express Error] Path: ${req.path}, Error:`, err); // Avoid leaking stack trace to client if (res.headersSent) { return next(err); } res.status(500).json({ error: 'Internal Server Error' }); }); http.listen(Number(process.env.PORT || 8080), '0.0.0.0');