diff --git a/server-db.ts b/server-db.ts index b0afe87..9aefbb5 100644 --- a/server-db.ts +++ b/server-db.ts @@ -153,8 +153,18 @@ export function addCaddyRoute(hostname: string, upstream: string, tls: boolean, return db.prepare('SELECT * FROM caddy WHERE id = ?').get(lastInsertRowid) as CaddyRoute; } +export function updateCaddyRoute(id: number, hostname: string, upstream: string, tls: boolean, compress: boolean): CaddyRoute { + db.prepare('UPDATE caddy SET hostname = ?, upstream = ?, tls = ?, compress = ? WHERE id = ?') + .run(hostname, upstream, tls ? 1 : 0, compress ? 1 : 0, id); + return db.prepare('SELECT * FROM caddy WHERE id = ?').get(id) as CaddyRoute; +} + export function deleteCaddyRoute(id: number): void { db.prepare('DELETE FROM caddy WHERE id = ?').run(id); } +export function getCaddyRouteById(id: number): CaddyRoute | undefined { + return db.prepare('SELECT * FROM caddy WHERE id = ?').get(id) as CaddyRoute | undefined; +} + export default db; diff --git a/server.ts b/server.ts index 9413871..aa8e08c 100644 --- a/server.ts +++ b/server.ts @@ -7,7 +7,7 @@ import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import DatabaseConstructor from 'better-sqlite3'; import { ConfidentialClientApplication } from '@azure/msal-node'; -import db, { getSetting, setSetting, getAllSettings, getCaddyRoutes, addCaddyRoute, deleteCaddyRoute, DB_FILE } from './server-db'; +import db, { getSetting, setSetting, getAllSettings, getCaddyRoutes, addCaddyRoute, updateCaddyRoute, deleteCaddyRoute, getCaddyRouteById, DB_FILE } from './server-db'; import { Device, LabTemplate, Booking, LogEntry, User, QuickLink } from './src/types'; const uid = (prefix: string) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; @@ -89,11 +89,12 @@ function buildCaddyfile(): string { return lines.join('\n'); } -function importCaddyfileRoutes(): void { +function importCaddyfileRoutes(userId: string): void { if (getCaddyRoutes().length > 0) return; const caddyfilePath = '/etc/caddy/Caddyfile'; if (!fs.existsSync(caddyfilePath)) return; const lines = fs.readFileSync(caddyfilePath, 'utf-8').split('\n'); + const imported: string[] = []; let i = 0; while (i < lines.length) { const line = lines[i].trim(); @@ -113,11 +114,17 @@ function importCaddyfileRoutes(): void { const tls = /tls\s+internal/.test(block); const compress = /encode/.test(block); addCaddyRoute(hostname, upstream, tls, compress); - console.log(`[Caddy] Imported route from Caddyfile: ${hostname} → ${upstream}`); + imported.push(`${hostname} → ${upstream}`); } } i++; } + if (imported.length > 0) { + db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)') + .run(uid('log'), new Date().toISOString(), 'system', + `Caddy: imported ${imported.length} route(s) from Caddyfile — ${imported.join(', ')}`, + null, userId); + } } async function pushCaddyConfig(): Promise { @@ -143,7 +150,7 @@ async function startServer() { if (cnt === 0) { const passwordHash = bcrypt.hashSync('admin', 10); db.prepare('INSERT INTO users (id, name, role, email, password_hash) VALUES (?, ?, ?, ?, ?)') - .run(uid('u'), 'Admin', 'Admin', 'admin@ghostgrid.local', passwordHash); + .run(uid('u'), 'admin', 'admin', 'admin@ghostgrid.local', passwordHash); console.log('[Init] Default admin user created — login: admin@ghostgrid.local / admin'); } @@ -331,7 +338,7 @@ async function startServer() { } } if (!caddyWasEnabled && updates.caddy_enabled === 'true') { - importCaddyfileRoutes(); + importCaddyfileRoutes(req.user!.userId); } pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after settings save:', err.message)); res.json(maskSettings(getAllSettings())); @@ -1182,6 +1189,9 @@ async function startServer() { }; if (!hostname || !upstream) return res.status(400).json({ error: 'hostname and upstream are required.' }); const route = addCaddyRoute(hostname.trim(), upstream.trim(), tls !== false, compress !== false); + db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)') + .run(uid('log'), new Date().toISOString(), 'system', + `Caddy route added: ${hostname.trim()} → ${upstream.trim()}`, null, req.user!.userId); pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route add:', err.message)); res.json(route); } catch (err: any) { @@ -1189,11 +1199,36 @@ async function startServer() { } }); + app.put('/api/caddy/routes/:id', requireAuth, async (req, res) => { + try { + const id = Number(req.params.id); + if (!id) return res.status(400).json({ error: 'Invalid route id.' }); + const { hostname, upstream, tls, compress } = req.body as { + hostname?: string; upstream?: string; tls?: boolean; compress?: boolean; + }; + if (!hostname || !upstream) return res.status(400).json({ error: 'hostname and upstream are required.' }); + const route = updateCaddyRoute(id, hostname.trim(), upstream.trim(), tls !== false, compress !== false); + db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)') + .run(uid('log'), new Date().toISOString(), 'system', + `Caddy route updated: ${hostname.trim()} → ${upstream.trim()}`, null, req.user!.userId); + pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route update:', err.message)); + res.json(route); + } catch (err: any) { + res.status(500).json({ error: err.message }); + } + }); + app.delete('/api/caddy/routes/:id', requireAuth, (req, res) => { try { const id = Number(req.params.id); if (!id) return res.status(400).json({ error: 'Invalid route id.' }); + const existing = getCaddyRouteById(id); deleteCaddyRoute(id); + if (existing) { + db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)') + .run(uid('log'), new Date().toISOString(), 'system', + `Caddy route deleted: ${existing.hostname} → ${existing.upstream}`, null, req.user!.userId); + } pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route delete:', err.message)); res.status(204).send(); } catch (err: any) { diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 1710726..00dc9b8 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -4,7 +4,7 @@ import { User } from '../types'; import { Shield, Activity, Save, CheckCircle, AlertCircle, Eye, EyeOff, Settings2, KeyRound, Globe, Clock, ChevronRight, Copy, Users, RefreshCw, Terminal, - Network, Trash2, Plus, HardDrive, Download, Upload, AlertTriangle, Plug, Server, + Network, Trash2, Plus, HardDrive, Download, Upload, AlertTriangle, Plug, Server, Pencil, X, } from 'lucide-react'; const SECRET_SENTINEL = '__SET__'; @@ -200,6 +200,13 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { const [newTls, setNewTls] = useState(true); const [newCompress, setNewCompress] = useState(true); + const [editingRouteId, setEditingRouteId] = useState(null); + const [editHostname, setEditHostname] = useState(''); + const [editUpstream, setEditUpstream] = useState(''); + const [editTls, setEditTls] = useState(true); + const [editCompress, setEditCompress] = useState(true); + const [savingRoute, setSavingRoute] = useState(false); + const [dbInfo, setDbInfo] = useState(null); const [backingUp, setBackingUp] = useState(false); const [importFile, setImportFile] = useState(null); @@ -293,7 +300,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { setAzureClientSecret(''); setCheckmkApiSecret(''); setSemaphoreApiToken(''); - loadCaddyRoutes(); + await loadCaddyRoutes(); setSuccessMsg('Settings saved successfully.'); setTimeout(() => setSuccessMsg(''), 4000); } catch { @@ -406,6 +413,40 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { } } + function handleEditStart(r: CaddyRoute) { + setEditingRouteId(r.id); + setEditHostname(r.hostname); + setEditUpstream(r.upstream); + setEditTls(r.tls === 1); + setEditCompress(r.compress === 1); + } + + function handleEditCancel() { + setEditingRouteId(null); + } + + async function handleEditSave(id: number) { + if (!editHostname.trim() || !editUpstream.trim()) return; + setSavingRoute(true); + try { + const res = await authFetch(`/api/caddy/routes/${id}`, { + method: 'PUT', + body: JSON.stringify({ hostname: editHostname.trim(), upstream: editUpstream.trim(), tls: editTls, compress: editCompress }), + }); + if (!res.ok) { + const d = await res.json(); + setError(d.error || 'Failed to update route.'); + return; + } + setEditingRouteId(null); + await loadCaddyRoutes(); + } catch { + setError('Network error updating route.'); + } finally { + setSavingRoute(false); + } + } + async function loadDbInfo() { try { const res = await authFetch('/api/database/info'); @@ -901,22 +942,61 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { {/* Custom routes */} {caddyRoutes?.custom.map(r => ( -
-
- {r.hostname} - > - {r.upstream} - {r.tls ? TLS : null} - {r.compress ? GZ : null} -
- +
+ {editingRouteId === r.id ? ( +
+
+ + +
+
+ + +
+
+ TLS + +
+
+ GZ + +
+ + +
+ ) : ( +
+
+ {r.hostname} + > + {r.upstream} + {r.tls ? TLS : null} + {r.compress ? GZ : null} +
+
+ + +
+
+ )}
))}