@gacua/backend
Version:
GACUA Backend
184 lines ⢠7.08 kB
JavaScript
/**
* @license
* Copyright 2025 MuleRun
* SPDX-License-Identifier: Apache-2.0
*/
import express from 'express';
import os from 'os';
import path from 'path';
import { fileURLToPath } from 'url';
import { createServer } from 'http';
import { randomUUID } from 'crypto';
import { pinoHttp } from 'pino-http';
import qrcode from 'qrcode-terminal';
import { logger } from './logger.js';
import { setupWebSocketServer } from './ws/index.js';
import { sessionManager } from './services/session/index.js';
import { sessionRepository } from './repository/index.js';
import { regenerateToken, validateTokenString, getAccessToken, } from './auth/token.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const port = process.env['PORT'] || 3000;
function validateToken(req, res, next) {
const token = req.query['token'];
if (!validateTokenString(token)) {
logger.warn({
method: req.method,
path: req.path,
ip: req.ip,
userAgent: req.get('user-agent'),
}, 'Invalid token attempt');
res.status(403).json({ error: 'Invalid or missing access token' });
return;
}
next();
}
const app = express();
const server = createServer(app);
app.use(pinoHttp({
logger,
genReqId: (req) => {
return (req.get('x-request-id') || req.get('x-correlation-id') || randomUUID());
},
customLogLevel: (req, res) => {
const code = res.statusCode || 500;
if (code >= 400)
return 'error';
if (code === 304)
return 'debug';
if (code >= 300)
return 'warn';
return 'debug';
},
}));
app.use(express.json());
app.get('/api/health', validateToken, (req, res) => {
res.json({ message: 'healthy' });
});
app.get('/api/sessions', validateToken, async (req, res) => {
try {
const sessions = await sessionManager.getAllSessions();
res.json(sessions);
}
catch (error) {
req.log.error({ err: error }, 'Failed to retrieve sessions');
res.status(500).json({ error: 'Failed to retrieve sessions' });
}
});
app.post('/api/sessions', validateToken, async (req, res) => {
try {
const requestBody = req.body;
const result = await sessionManager.createSession(requestBody);
res.status(201).json(result);
}
catch (error) {
req.log.error({ err: error }, 'Failed to create session');
res.status(500).json({ error: 'Failed to create session' });
}
});
app.get('/api/sessions/:id', validateToken, async (req, res) => {
try {
const sessionId = req.params['id'];
const metadata = await sessionManager.getSession(sessionId);
res.json(metadata);
}
catch (error) {
req.log.warn({ sessionId: req.params['id'], err: error }, 'Session not found');
res.status(404).json({ error: 'Session not found' });
}
});
app.get('/api/sessions/:id/messages', validateToken, async (req, res) => {
try {
const sessionId = req.params['id'];
const messages = await sessionManager.getMessages(sessionId);
res.json(messages);
}
catch (error) {
req.log.error({ sessionId: req.params['id'], err: error }, 'Failed to retrieve messages');
res.status(500).json({ error: 'Failed to retrieve messages' });
}
});
app.get('/images/:sessionId/:fileName', validateToken, async (req, res) => {
try {
const { sessionId, fileName } = req.params;
const imageBuffer = await sessionRepository.getImage(sessionId, fileName);
res.set({
'Content-Type': 'image/*',
'Cache-Control': 'public, max-age=86400',
});
res.send(imageBuffer);
}
catch (error) {
req.log.warn({
sessionId: req.params['sessionId'],
fileName: req.params['fileName'],
err: error,
}, 'Image not found');
res.status(404).json({ error: 'Image not found' });
}
});
setupWebSocketServer(server);
app.use(express.static(path.join(__dirname, 'public')));
function findLanIpAddress() {
const allIps = [];
const interfaces = os.networkInterfaces();
let bestGuess = null;
const potentialInterfaces = Object.values(interfaces)
.flatMap((interfaceList) => interfaceList)
.filter((info) => !!info)
.filter((info) => info.family === 'IPv4' && !info.internal);
potentialInterfaces.forEach((info) => {
allIps.push(info.address);
});
if (allIps.length === 0) {
return { bestGuess: null, allIps: [] };
}
// Priority 1: Find IPs in the most common home network range (192.168.x.x)
const homeNetworkIp = potentialInterfaces.find((info) => info.address.startsWith('192.168.'));
if (homeNetworkIp) {
bestGuess = homeNetworkIp.address;
}
// Priority 2 (if no 192.168 found): Check for other private ranges, avoiding common Docker/VM ranges
if (!bestGuess) {
const privateIp = potentialInterfaces.find((info) => info.address.startsWith('10.') || info.address.startsWith('172.'));
// Be wary of default Docker bridge network (172.17.x.x)
if (privateIp && !privateIp.address.startsWith('172.17.')) {
bestGuess = privateIp.address;
}
}
// Priority 3 (Last resort): If no ideal IP was found, just take the first one available
if (!bestGuess) {
bestGuess = allIps[0] || null;
}
return { bestGuess, allIps };
}
export function startServer() {
logger.info('Starting GACUA server...');
regenerateToken();
server.listen(port, () => {
logger.info({ port }, 'GACUA server started successfully');
const { bestGuess, allIps } = findLanIpAddress();
if (bestGuess) {
const serverUrl = `http://${bestGuess}:${port}?token=${getAccessToken()}`;
console.log('\nš --- Primary Access URL (Magic Link) --- š');
console.log(`Scan the QR code with your phone to access GACUA:`);
qrcode.generate(serverUrl, { small: true });
console.log(`Or open this URL in your phone's browser: ${serverUrl}`);
console.log('š This URL includes a secure access token valid for 24 hours');
console.log('------------------------------------');
const otherIps = allIps.filter((ip) => ip !== bestGuess);
if (otherIps.length > 0) {
console.log('\nš Other potential addresses on this machine:');
otherIps.forEach((ip) => {
console.log(` - http://${ip}:${port}?token=${getAccessToken()}`);
});
console.log('\nIf the QR code does not work, try one of the URLs above.');
}
}
else {
console.warn('\nā ļø Could not find a suitable network IP address.');
console.warn(`You can access the server from this PC at: http://localhost:${port}?token=${getAccessToken()}`);
}
});
}
//# sourceMappingURL=server.js.map