124 lines
3.8 KiB
JavaScript
124 lines
3.8 KiB
JavaScript
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';
|
|
import consoleApiRouter from './routes/consoleApi.js';
|
|
|
|
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(express.json());
|
|
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();
|
|
};
|
|
|
|
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');
|