Break the server down in different files and change fetch all party lines behavior

This commit is contained in:
pedrocx486 2025-01-31 01:35:01 -03:00
parent cf70e11fe8
commit f4258fb35f
8 changed files with 284 additions and 233 deletions

View file

@ -0,0 +1,98 @@
import { Router } from 'express';
import { query, validationResult } from 'express-validator';
import { partyLines } from '../stores/dataStore';
import { v4 as uuidv4 } from 'uuid';
import { Client } from "../types/partyLine";
const router = Router();
// Route to verify if a party line exists before connecting
router.get('/joinPartyLine', [
query('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
], (req: any, res: any) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const currentPartyLine = req.query.partyLine;
const partyLine = partyLines[currentPartyLine];
if (!partyLine) {
return res.status(404).send({ status: 'Party line not found' });
}
res.status(200).send({ status: 'Connection to party line authorized' });
} catch (error) {
console.error('ERROR: Failed to join party line', error);
res.status(500).send({ status: 'Internal server error' });
}
});
// Route to connect to party line and receive rumors
router.get('/connectPartyLine', [
query('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
], (req: any, res: any) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const connectedPartyLine = req.query.partyLine;
const partyLine = partyLines[connectedPartyLine];
if (!partyLine) {
return res.status(404).send({ status: 'Party line not found' });
}
// Set headers for Server-Sent Events (SSE)
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const clientId = `${uuidv4()}`;
const client: Client = { clientId, response: res, ipAddress: res.socket.remoteAddress };
// Add client to the party line
partyLine.clients.push(client);
partyLine.lastActivity = Date.now();
console.log(`JOIN: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
// Send the last event to the client
res.write(`data: ${partyLine.lastEvent}\n\n`);
// Keep-alive mechanism to keep the connection open
const keepAliveId = setInterval(() => {
res.write(': keep-alive\n\n');
}, 15000);
// Function to remove the client from the party line
const removeClient = () => {
clearInterval(keepAliveId);
const index = partyLine.clients.findIndex((client: any) => client.clientId === clientId);
if (index !== -1) {
partyLine.clients.splice(index, 1);
console.log(`REMOVE: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
} else {
console.log(`CLIENT_NOT_FOUND: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
}
try {
res.write('event: close\ndata: finished\n\n');
res.end();
} catch (err) {
console.error(`ERROR: Error sending close event for client ${clientId}:`, err);
}
console.log(`DISCONNECT: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
};
// Handle client disconnection
req.on('close', removeClient);
req.on('error', (err: any) => {
console.error(`ERROR: Error in client connection in party line ${connectedPartyLine}:`, err);
removeClient();
});
} catch (error) {
console.error('ERROR: Failed to connect to party line', error);
res.status(500).send({ status: 'Internal server error' });
}
});
export default router;

View file

@ -0,0 +1,115 @@
import { Router } from 'express';
import { body, validationResult } from 'express-validator';
import { MAX_PARTY_LINES, INITIAL_RUMOR } from '../stores/configStore';
import { partyLines } from '../stores/dataStore';
import { broadcast } from '../services/broadcastService';
const router = Router();
// Route to get all party lines
router.get('/partyLines', (_req: any, res: any) => {
const allPartyLines = Object.keys(partyLines).map(key => ({
name: key,
...partyLines[key],
clients: partyLines[key].clients.map(({ response, ...client }) => client)
}));
res.status(200).send(allPartyLines);
});
// Route to create a new party line
router.post('/createPartyLine', [
body('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
], (req: any, res: any) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const { partyLine: currentPartyLine } = req.body;
if (Object.keys(partyLines).length >= MAX_PARTY_LINES) {
return res.status(400).send({ status: 'Maximum number of party lines reached' });
}
if (partyLines[currentPartyLine]) {
return res.status(400).send({ status: 'Party line already exists' });
}
// Create a new party line
partyLines[currentPartyLine] = {
clients: [],
lastEvent: INITIAL_RUMOR,
lastActivity: Date.now()
};
console.log(`CREATE: { partyLine: ${currentPartyLine} }`);
res.status(200).send({ status: 'Party line created', currentPartyLine });
} catch (error) {
console.error('ERROR: Failed to create party line', error);
res.status(500).send({ status: 'Internal server error' });
}
});
// Route to delete a party line
router.delete('/deletePartyLine', [
body('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
], (req: any, res: any) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const { partyLine: currentPartyLine } = req.body;
const partyLine = partyLines[currentPartyLine];
if (!partyLine) {
return res.status(404).send({ status: 'Party line not found' });
}
// Broadcast deletion message to all clients
broadcast(currentPartyLine, 'PARTY_LINE_DELETED');
// Disconnect all clients
partyLine.clients.forEach((client: any) => {
console.log(`DISCONNECTING: { partyLine: ${currentPartyLine}, clientId: ${client.clientId} }`);
client.response.end();
});
// Delete the party line
delete partyLines[currentPartyLine];
console.log(`DELETE: { partyLine: ${currentPartyLine} }`);
res.status(200).send({ status: 'Party line deleted', currentPartyLine });
} catch (error) {
console.error('ERROR: Failed to delete party line', error);
res.status(500).send({ status: 'Internal server error' });
}
});
// Route to send an event to a party line
router.post('/rumor', [
body('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name'),
body('rumor').isString().trim().escape().notEmpty().withMessage('Invalid rumor')
], (req: any, res: any) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const { partyLine: connectedPartyLine, rumor } = req.body;
const partyLine = partyLines[connectedPartyLine];
if (!partyLine) {
return res.status(404).send({ status: 'Party line not found' });
}
console.log(`RECEIVE: { partyLine: ${connectedPartyLine}, rumor: ${rumor} }`);
// Update the last event and broadcast it to all clients
partyLine.lastEvent = rumor;
partyLine.clients.forEach((client: any) => {
client.response.write(`data: ${rumor}\n\n`);
});
res.status(200).send({ status: 'Rumor broadcast' });
} catch (error) {
console.error('ERROR: Failed to send event', error);
res.status(500).send({ status: 'Internal server error' });
}
});
export default router;

View file

@ -0,0 +1,11 @@
import { Router } from 'express';
import path from 'path';
const router = Router();
// Navigate to the admin route in the client
router.get('/admin', (_req: any, res: any) => {
res.sendFile(path.resolve(__dirname, '../../static', 'index.html'));
});
export default router;

View file

@ -1,21 +1,21 @@
import express from 'express'; import express from 'express';
import { body, query, validationResult } from 'express-validator';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
import cors from 'cors'; import cors from 'cors';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import { v4 as uuidv4 } from 'uuid';
import dotenv from 'dotenv';
import helmet from 'helmet'; import helmet from 'helmet';
import path from 'path'; import path from 'path';
import { partyLines } from './stores/dataStore';
dotenv.config(); import { ENVIRONMENT, PORT, CLIENT_URL, DOCKER } from './stores/configStore';
import { broadcast } from './services/broadcastService';
import connectionRoutes from './routes/connectionRoutes';
import managementRoutes from './routes/managementRoutes';
import navigationRoutes from './routes/navigationRoutes';
const app = express(); const app = express();
const PORT = Number(process.env.PORT);
// CORS configuration options // CORS configuration options
const corsOptions = { const corsOptions = {
origin: process.env.CLIENT_URL, origin: CLIENT_URL,
optionsSuccessStatus: 200, optionsSuccessStatus: 200,
methods: ['GET', 'POST', 'DELETE'], methods: ['GET', 'POST', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'] allowedHeaders: ['Content-Type', 'Authorization']
@ -38,7 +38,7 @@ app.use(limiter);
app.use('/', express.static(path.join(__dirname, '../static'))); app.use('/', express.static(path.join(__dirname, '../static')));
// Force HTTPS redirection and CSP in production // Force HTTPS redirection and CSP in production
if (process.env.ENVIRONMENT === 'production') { if (ENVIRONMENT === 'production') {
app.use((req: any, res: any, next: any) => { app.use((req: any, res: any, next: any) => {
if (req.headers['x-forwarded-proto'] !== 'https') { if (req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect(`https://${req.headers.host}${req.url}`); return res.redirect(`https://${req.headers.host}${req.url}`);
@ -57,7 +57,7 @@ if (process.env.ENVIRONMENT === 'production') {
}) })
); );
} else { } else {
// Disable https preference for non-prod // Disable CSP for non-prod
app.use( app.use(
helmet({ helmet({
contentSecurityPolicy: false, contentSecurityPolicy: false,
@ -65,131 +65,17 @@ if (process.env.ENVIRONMENT === 'production') {
); );
} }
// Navigate to the admin route in the client // Routes
app.get('/admin', (_req: any, res: any) => { app.use(navigationRoutes);
res.sendFile(path.resolve(__dirname, '../static', 'index.html')); app.use(managementRoutes);
}); app.use(connectionRoutes);
// Maximum number of party lines allowed // Periodically resend the last rumor and clean up inactive party lines
const MAX_PARTY_LINES = Number(process.env.MAX_PARTY_LINES);
// In-memory storage for party lines
const partyLines = {};
// Function to broadcast a rumor to all clients at a party line
const broadcast = (currentPartyLine: string, rumor: string) => {
const partyLine = partyLines[currentPartyLine];
if (partyLine) {
console.log(`BROADCAST: { partyLine: ${currentPartyLine}, rumor: ${rumor} }`);
partyLine.clients.forEach((client: any) => {
if (client.response && typeof client.response.write === 'function') {
try {
client.response.write(`data: ${rumor}\n\n`);
} catch (err) {
console.error(`ERROR: Failed to write to client ${client.clientId}`, err);
}
} else {
console.error(`ERROR: Response object is invalid for client ${client.clientId}`);
}
});
}
};
// Route to verify if a party line exists before connecting
app.get('/joinPartyLine', [
query('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
], (req: any, res: any) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const currentPartyLine = req.query.partyLine;
const partyLine = partyLines[currentPartyLine];
if (!partyLine) {
return res.status(404).send({ status: 'Party line not found' });
}
res.status(200).send({ status: 'Connection to party line authorized' });
} catch (error) {
console.error('ERROR: Failed to join party line', error);
res.status(500).send({ status: 'Internal server error' });
}
});
// Route to connect to party line and receive rumors
app.get('/connectPartyLine', [
query('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
], (req: any, res: any) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const connectedPartyLine = req.query.partyLine;
const partyLine = partyLines[connectedPartyLine];
if (!partyLine) {
return res.status(404).send({ status: 'Party line not found' });
}
// Set headers for Server-Sent Events (SSE)
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const clientId = `${uuidv4()}`;
const client = { clientId, response: res };
// Add client to the party line
partyLine.clients.push(client);
partyLine.lastActivity = Date.now();
console.log(`JOIN: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
// Send the last event to the client
res.write(`data: ${partyLine.lastEvent}\n\n`);
// Keep-alive mechanism to keep the connection open
const keepAliveId = setInterval(() => {
res.write(': keep-alive\n\n');
}, 15000);
// Function to remove the client from the party line
const removeClient = () => {
clearInterval(keepAliveId);
const index = partyLine.clients.findIndex((client: any) => client.clientId === clientId);
if (index !== -1) {
partyLine.clients.splice(index, 1);
console.log(`REMOVE: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
} else {
console.log(`CLIENT_NOT_FOUND: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
}
try {
res.write('event: close\ndata: finished\n\n');
res.end();
} catch (err) {
console.error(`ERROR: Error sending close event for client ${clientId}:`, err);
}
console.log(`DISCONNECT: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
};
// Handle client disconnection
req.on('close', removeClient);
req.on('error', (err: any) => {
console.error(`ERROR: Error in client connection in party line ${connectedPartyLine}:`, err);
removeClient();
});
} catch (error) {
console.error('ERROR: Failed to connect to party line', error);
res.status(500).send({ status: 'Internal server error' });
}
});
// Periodically resend the last event and clean up inactive party lines
setInterval(() => { setInterval(() => {
const now = Date.now(); const now = Date.now();
Object.keys(partyLines).forEach((currentPartyLine) => { Object.keys(partyLines).forEach((currentPartyLine) => {
const partyLine = partyLines[currentPartyLine]; const partyLine = partyLines[currentPartyLine];
if (partyLine.lastEvent) { if (partyLine.lastEvent && partyLine.clients.length > 0) {
broadcast(currentPartyLine, partyLine.lastEvent); broadcast(currentPartyLine, partyLine.lastEvent);
} }
if (now - partyLine.lastActivity > 3600000) { // 1 hour in milliseconds if (now - partyLine.lastActivity > 3600000) { // 1 hour in milliseconds
@ -199,113 +85,11 @@ setInterval(() => {
}); });
}, 30000); }, 30000);
// Route to create a new party line
app.post('/createPartyLine', [
body('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
], (req: any, res: any) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const { partyLine: currentPartyLine } = req.body;
if (Object.keys(partyLines).length >= MAX_PARTY_LINES) {
return res.status(400).send({ status: 'Maximum number of party lines reached' });
}
if (partyLines[currentPartyLine]) {
return res.status(400).send({ status: 'Party line already exists' });
}
// Create a new party line
partyLines[currentPartyLine] = {
clients: [],
lastEvent: process.env.INITIAL_RUMOR,
lastActivity: Date.now()
};
console.log(`CREATE: { partyLine: ${currentPartyLine} }`);
res.status(200).send({ status: 'Party line created', currentPartyLine });
} catch (error) {
console.error('ERROR: Failed to create party line', error);
res.status(500).send({ status: 'Internal server error' });
}
});
// Route to send an event to a party line
app.post('/rumor', [
body('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name'),
body('rumor').isString().trim().escape().notEmpty().withMessage('Invalid rumor')
], (req: any, res: any) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const { partyLine: connectedPartyLine, rumor } = req.body;
const partyLine = partyLines[connectedPartyLine];
if (!partyLine) {
return res.status(404).send({ status: 'Party line not found' });
}
console.log(`RECEIVE: { partyLine: ${connectedPartyLine}, rumor: ${rumor} }`);
// Update the last event and broadcast it to all clients
partyLine.lastEvent = rumor;
partyLine.clients.forEach((client: any) => {
client.response.write(`data: ${rumor}\n\n`);
});
res.status(200).send({ status: 'Rumor broadcast' });
} catch (error) {
console.error('ERROR: Failed to send event', error);
res.status(500).send({ status: 'Internal server error' });
}
});
// Route to get all party lines
app.get('/partyLines', (_req: any, res: any) => {
const allPartyLines = Object.keys(partyLines);
res.status(200).send({ allPartyLines });
});
// Route to delete a party line
app.delete('/deletePartyLine', [
body('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
], (req: any, res: any) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const { partyLine: currentPartyLine } = req.body;
const partyLine = partyLines[currentPartyLine];
if (!partyLine) {
return res.status(404).send({ status: 'Party line not found' });
}
// Broadcast deletion message to all clients
broadcast(currentPartyLine, 'PARTY_LINE_DELETED');
// Disconnect all clients
partyLine.clients.forEach((client: any) => {
console.log(`DISCONNECTING: { partyLine: ${currentPartyLine}, clientId: ${client.clientId} }`);
client.response.end();
});
// Delete the party line
delete partyLines[currentPartyLine];
console.log(`DELETE: { partyLine: ${currentPartyLine} }`);
res.status(200).send({ status: 'Party line deleted', currentPartyLine });
} catch (error) {
console.error('ERROR: Failed to delete party line', error);
res.status(500).send({ status: 'Internal server error' });
}
});
// Start the server // Start the server
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Rumor Party Line`); console.log(`Rumor Party Line`);
console.log(`Internally running on http://localhost:${PORT} in ${process.env.ENVIRONMENT} mode.`); console.log(`Internally running on http://localhost:${PORT} in ${ENVIRONMENT} mode.`);
if (process.env.DOCKER) { if (DOCKER) {
console.log(`You're running on Docker!\n↳ Check your external port in the compose file to avoid confusion!`); console.log(`You're running on Docker!\n↳ Check your external port in the compose file to avoid confusion!`);
} }
}); });

View file

@ -0,0 +1,20 @@
import { partyLines } from '../stores/dataStore';
// Function to broadcast a rumor to all clients at a party line
export const broadcast = (currentPartyLine: string, rumor: string) => {
const partyLine = partyLines[currentPartyLine];
if (partyLine) {
console.log(`BROADCAST: { partyLine: ${currentPartyLine}, rumor: ${rumor} }`);
partyLine.clients.forEach((client: any) => {
if (client.response && typeof client.response.write === 'function') {
try {
client.response.write(`data: ${rumor}\n\n`);
} catch (err) {
console.error(`ERROR: Failed to write to client ${client.clientId}`, err);
}
} else {
console.error(`ERROR: Response object is invalid for client ${client.clientId}`);
}
});
}
};

View file

@ -0,0 +1,9 @@
import dotenv from 'dotenv';
dotenv.config();
export const ENVIRONMENT = process.env.ENVIRONMENT || 'development';
export const PORT = Number(process.env.PORT) || 3000;
export const CLIENT_URL = process.env.CLIENT_URL || 'http://localhost:3000';
export const MAX_PARTY_LINES = Number(process.env.MAX_PARTY_LINES) || 1;
export const INITIAL_RUMOR = process.env.INITIAL_RUMOR || 'Lorem Ipsum.';
export const DOCKER = Boolean(process.env.DOCKER);

View file

@ -0,0 +1,3 @@
import { PartyLine } from "../types/partyLine";
export const partyLines: PartyLine[] = [];

View file

@ -0,0 +1,11 @@
export type PartyLine = {
clients: Client[];
lastActivity: number;
lastEvent: string;
}
export type Client = {
clientId: string;
response: any;
ipAddress: string;
}