import express, { type Request, type Response } from 'express'; import fs from 'node:fs/promises'; import path from 'node:path'; import type { ControlPlaneState, CreateUserInput, PanelLoginInput, UpdateSystemInput, } from '../src/shared/contracts'; import { validateCreateUserInput, validateSystemInput } from '../src/shared/validation'; import { buildRuntimePaths, createUserRecord, render3proxyConfig, type RuntimePaths, } from './lib/config'; import { getDashboardSnapshot } from './lib/snapshot'; import { AuthService } from './lib/auth'; import type { LiveSyncPublisher } from './lib/liveSync'; import type { RuntimeController } from './lib/runtime'; import { StateStore } from './lib/store'; export interface AppServices { store: StateStore; runtime: RuntimeController; runtimeRootDir: string; auth: AuthService; liveSync?: LiveSyncPublisher; } export function createApp({ store, runtime, runtimeRootDir, auth, liveSync }: AppServices) { const app = express(); const runtimePaths = buildRuntimePaths(runtimeRootDir); const distDir = path.resolve('dist'); app.use(express.json()); app.use(express.static(distDir)); app.post('/api/auth/login', (request, response) => { const input = request.body as Partial; try { const payload = auth.login(input.login?.trim() ?? '', input.password ?? ''); response.json(payload); } catch (error) { response.status(401).json({ error: error instanceof Error ? error.message : 'Wrong panel credentials.', }); } }); app.use('/api', (request, response, next) => { if (request.path === '/auth/login' || request.path === '/health') { next(); return; } const token = auth.extractBearerToken(request); if (!token) { response.status(401).json(auth.unauthorizedError()); return; } if (!auth.verify(token)) { response.status(401).json(auth.invalidTokenError()); return; } next(); }); app.get('/api/health', async (_request, response) => { const state = await store.read(); const previewConfig = render3proxyConfig(state, runtimePaths); response.json({ ok: true, runtime: runtime.getSnapshot(), users: state.userRecords.length, configBytes: Buffer.byteLength(previewConfig), }); }); app.get('/api/state', async (_request, response, next) => { try { const payload = await getDashboardSnapshot(store, runtime, runtimePaths); response.json(payload); } catch (error) { next(error); } }); app.post('/api/runtime/:action', async (request, response, next) => { try { const state = await store.read(); const action = request.params.action; const controller = action === 'restart' ? runtime.restart.bind(runtime) : action === 'stop' ? runtime.stop.bind(runtime) : runtime.start.bind(runtime); if (!['start', 'restart', 'stop'].includes(action)) { response.status(404).json({ error: 'Unknown runtime action.' }); return; } const runtimeSnapshot = await controller(); state.service.lastEvent = action === 'restart' ? 'Runtime restarted from panel' : action === 'stop' ? 'Runtime stop requested from panel' : 'Runtime start requested from panel'; state.service.startedAt = runtimeSnapshot.startedAt ?? null; await writeConfigAndState(store, state, runtimePaths); liveSync?.notifyPotentialChange(); response.json(await getDashboardSnapshot(store, runtime, runtimePaths)); } catch (error) { next(error); } }); app.post('/api/users', async (request, response, next) => { try { const state = await store.read(); const input = validateCreateUserInput(request.body as Partial, state.system.services); const record = createUserRecord(state, input); state.userRecords.push(record); state.service.lastEvent = `User ${record.username} created from panel`; await persistRuntimeMutation(store, runtime, state, runtimePaths); liveSync?.notifyPotentialChange(); response.status(201).json(await getDashboardSnapshot(store, runtime, runtimePaths)); } catch (error) { next(error); } }); app.put('/api/users/:id', async (request, response, next) => { try { const state = await store.read(); const user = state.userRecords.find((entry) => entry.id === request.params.id); if (!user) { response.status(404).json({ error: 'User not found.' }); return; } const input = validateCreateUserInput(request.body as Partial, state.system.services); const duplicateUser = state.userRecords.find( (entry) => entry.id !== user.id && entry.username.toLowerCase() === input.username.toLowerCase(), ); if (duplicateUser) { response.status(400).json({ error: 'Username already exists.' }); return; } user.username = input.username; user.password = input.password; user.serviceId = input.serviceId; user.quotaBytes = input.quotaMb === null ? null : input.quotaMb * 1024 * 1024; state.service.lastEvent = `User ${user.username} updated from panel`; await persistRuntimeMutation(store, runtime, state, runtimePaths); liveSync?.notifyPotentialChange(); response.json(await getDashboardSnapshot(store, runtime, runtimePaths)); } catch (error) { next(error); } }); app.put('/api/system', async (request, response, next) => { try { const state = await store.read(); const requestedSystem = request.body as Partial; const nextServiceIds = new Set( (Array.isArray(requestedSystem.services) ? requestedSystem.services : []).map((service) => service.id), ); const removedServiceIds = new Set( state.system.services .map((service) => service.id) .filter((serviceId) => !nextServiceIds.has(serviceId)), ); const removedUsers = state.userRecords.filter((user) => removedServiceIds.has(user.serviceId)); state.userRecords = state.userRecords.filter((user) => !removedServiceIds.has(user.serviceId)); state.system = validateSystemInput(requestedSystem, state.userRecords); state.service.lastEvent = removedUsers.length > 0 ? `System configuration updated from panel and removed ${removedUsers.length} linked users` : 'System configuration updated from panel'; await persistRuntimeMutation(store, runtime, state, runtimePaths); liveSync?.notifyPotentialChange(); response.json(await getDashboardSnapshot(store, runtime, runtimePaths)); } catch (error) { next(error); } }); app.post('/api/users/:id/pause', async (request, response, next) => { try { const state = await store.read(); const user = state.userRecords.find((entry) => entry.id === request.params.id); if (!user) { response.status(404).json({ error: 'User not found.' }); return; } user.paused = !user.paused; state.service.lastEvent = user.paused ? `User ${user.username} paused from panel` : `User ${user.username} resumed from panel`; await persistRuntimeMutation(store, runtime, state, runtimePaths); liveSync?.notifyPotentialChange(); response.json(await getDashboardSnapshot(store, runtime, runtimePaths)); } catch (error) { next(error); } }); app.delete('/api/users/:id', async (request, response, next) => { try { const state = await store.read(); const index = state.userRecords.findIndex((entry) => entry.id === request.params.id); if (index === -1) { response.status(404).json({ error: 'User not found.' }); return; } const [removed] = state.userRecords.splice(index, 1); state.service.lastEvent = `User ${removed.username} deleted from panel`; await persistRuntimeMutation(store, runtime, state, runtimePaths); liveSync?.notifyPotentialChange(); response.json(await getDashboardSnapshot(store, runtime, runtimePaths)); } catch (error) { next(error); } }); app.use(async (_request, response) => { const distPath = path.join(distDir, 'index.html'); try { const html = await fs.readFile(distPath, 'utf8'); response.type('html').send(html); } catch { response.status(404).send('Frontend build not found.'); } }); app.use((error: unknown, _request: Request, response: Response, _next: express.NextFunction) => { response.status(400).json({ error: error instanceof Error ? error.message : 'Unknown server error.', }); }); return app; } async function persistRuntimeMutation( store: StateStore, runtime: RuntimeController, state: ControlPlaneState, runtimePaths: RuntimePaths, ) { await writeConfigAndState(store, state, runtimePaths); await runtime.reload(); } async function writeConfigAndState( store: StateStore, state: ControlPlaneState, runtimePaths: RuntimePaths, ) { const config = render3proxyConfig(state, runtimePaths); await fs.mkdir(path.dirname(runtimePaths.configPath), { recursive: true }); await fs.mkdir(path.dirname(runtimePaths.logPath), { recursive: true }); await fs.mkdir(runtimePaths.reportDir, { recursive: true }); await fs.writeFile(runtimePaths.configPath, config, 'utf8'); await store.write(state); }