From e32ebbc7349e71d30adaf46b7d861968685fd2c6 Mon Sep 17 00:00:00 2001 From: Philipp Date: Thu, 21 May 2026 10:14:33 +0200 Subject: [PATCH] - Passwort-Reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cd management npm run reset-password -- admin "neues-passwort" - Agent-Hardening backend/.env unterstützt jetzt: ALLOWED_MANAGEMENT_IPS="127.0.0.1,DEINE-MANAGEMENT-IP" Wenn gesetzt, akzeptiert der Agent nur Requests von diesen IPs. - Audit-Log Management speichert Aktionen wie Login, Logout, Node-Änderungen, Schedule-Updates, Backup/Restore, Settings-Änderungen. - Job-History Management speichert gestartete Backup/Restore/Scheduler-Jobs mit Node, VM, Typ, Agent-Job-ID und Status. - Operations-Seite Neue UI-Seite Operations mit: - Job History - Audit Log - systemd Templates deploy/systemd/incus-backup-agent.service deploy/systemd/incus-backup-management.service - Deployment-Doku docs/deployment.md --- .DS_Store | Bin 10244 -> 10244 bytes README.md | 11 +++ backend/.env.example | 1 + backend/src/config.js | 9 ++ backend/src/index.js | 8 ++ deploy/systemd/incus-backup-agent.service | 16 ++++ .../systemd/incus-backup-management.service | 16 ++++ docs/deployment.md | 82 ++++++++++++++++ frontend/src/App.jsx | 16 +++- frontend/src/components/Operations.jsx | 88 ++++++++++++++++++ management/package.json | 1 + management/src/db.js | 22 +++++ management/src/index.js | 2 + management/src/reset-password.js | 23 +++++ management/src/routes/auth.js | 15 +++ management/src/routes/nodes.js | 5 +- management/src/routes/operations.js | 12 +++ management/src/routes/proxy.js | 24 +++-- management/src/routes/schedules.js | 6 +- management/src/scheduler.js | 10 +- management/src/store.js | 63 +++++++++++++ 21 files changed, 417 insertions(+), 13 deletions(-) create mode 100644 deploy/systemd/incus-backup-agent.service create mode 100644 deploy/systemd/incus-backup-management.service create mode 100644 docs/deployment.md create mode 100644 frontend/src/components/Operations.jsx create mode 100644 management/src/reset-password.js create mode 100644 management/src/routes/operations.js diff --git a/.DS_Store b/.DS_Store index ee47e97e2f538bcc51d27cdaaf1e38ac59916fd6..9284dd227972dd8c326f03d2b55dde3c128729fb 100644 GIT binary patch delta 54 zcmZn(XbIS$DZsdEa;$hEuT*uluAza6iH?G)f!XAn;?j&glfR0~GxlzNB%sW{nO)%* K%jW+g%*+7cV-c|c delta 43 zcmZn(XbIS$DKI%syp+Sp+*n7!#K3&=ZE^d}&jl3tH?u1IVcBdYqsTN_Mf?!}GVBfO diff --git a/README.md b/README.md index cd8396e..2d7445e 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,13 @@ The management API stores nodes, users, sessions, and central schedules in SQLit The management API uses Node's built-in SQLite module and requires Node.js 22.5 or newer. +Reset an existing admin password without deleting the database: + +```bash +cd management +npm run reset-password -- admin "new-password" +``` + ## Frontend ```bash @@ -62,3 +69,7 @@ Changing most node-agent values applies to new API calls and jobs immediately. C ## Safety Notes Restore is intentionally guarded twice: the backend validates the snapshot against the VM, and the UI requires typing the VM name before sending the restore request. Restore jobs are never retried automatically. + +## Deployment + +See `docs/deployment.md` for systemd units, management/agent split, and production setup notes. diff --git a/backend/.env.example b/backend/.env.example index bb414a7..051175c 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -9,3 +9,4 @@ RESTIC_KEEP_WEEKLY=0 RESTIC_KEEP_MONTHLY=0 PORT=3000 API_TOKEN="" +ALLOWED_MANAGEMENT_IPS="" diff --git a/backend/src/config.js b/backend/src/config.js index d4b51af..a9053d2 100644 --- a/backend/src/config.js +++ b/backend/src/config.js @@ -17,6 +17,10 @@ export const requiredEnv = [ export const config = { port: Number(process.env.PORT || 3000), apiToken: process.env.API_TOKEN || '', + allowedManagementIps: (process.env.ALLOWED_MANAGEMENT_IPS || '') + .split(',') + .map((value) => value.trim()) + .filter(Boolean), zfsPoolName: process.env.ZFS_POOL_NAME || '', resticEnv: { AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID || '', @@ -44,6 +48,7 @@ export const editableEnv = [ { key: 'RESTIC_KEEP_MONTHLY', label: 'Keep monthly snapshots', required: false, secret: false }, { key: 'PORT', label: 'API port', required: false, secret: false }, { key: 'API_TOKEN', label: 'API token', required: false, secret: true }, + { key: 'ALLOWED_MANAGEMENT_IPS', label: 'Allowed management IPs', required: false, secret: false }, ]; export function missingEnvVars() { @@ -108,6 +113,10 @@ function applyRuntimeEnv(values) { } config.port = Number(process.env.PORT || 3000); config.apiToken = process.env.API_TOKEN || ''; + config.allowedManagementIps = (process.env.ALLOWED_MANAGEMENT_IPS || '') + .split(',') + .map((value) => value.trim()) + .filter(Boolean); config.zfsPoolName = process.env.ZFS_POOL_NAME || ''; config.resticEnv.AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || ''; config.resticEnv.AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || ''; diff --git a/backend/src/index.js b/backend/src/index.js index a5c5815..4a83f81 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -17,6 +17,10 @@ app.use(cors()); app.use(express.json()); app.use((req, res, next) => { + if (config.allowedManagementIps.length && !config.allowedManagementIps.includes(normalizeIp(req.ip))) { + res.status(403).json({ error: 'Forbidden management source.' }); + return; + } if (!config.apiToken) { next(); return; @@ -29,6 +33,10 @@ app.use((req, res, next) => { res.status(401).json({ error: 'Unauthorized.' }); }); +function normalizeIp(value) { + return String(value || '').replace(/^::ffff:/, ''); +} + app.use('/api/health', healthRouter); app.use('/api/vms', vmsRouter); app.use('/api/snapshots', snapshotsRouter); diff --git a/deploy/systemd/incus-backup-agent.service b/deploy/systemd/incus-backup-agent.service new file mode 100644 index 0000000..587f0d1 --- /dev/null +++ b/deploy/systemd/incus-backup-agent.service @@ -0,0 +1,16 @@ +[Unit] +Description=Incus Backup Node Agent +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=/opt/incus-backup-ui/backend +Environment=NODE_ENV=production +ExecStart=/usr/bin/npm start +Restart=on-failure +RestartSec=5 +User=root + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/incus-backup-management.service b/deploy/systemd/incus-backup-management.service new file mode 100644 index 0000000..b1dc941 --- /dev/null +++ b/deploy/systemd/incus-backup-management.service @@ -0,0 +1,16 @@ +[Unit] +Description=Incus Backup Management API +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=/opt/incus-backup-ui/management +Environment=NODE_ENV=production +ExecStart=/usr/bin/npm start +Restart=on-failure +RestartSec=5 +User=incus-backup + +[Install] +WantedBy=multi-user.target diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..825cce5 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,82 @@ +# Deployment + +## Node Agent + +Run this on every Incus host: + +```bash +cd /opt/incus-backup-ui/backend +cp .env.example .env +npm install +sudo npm start +``` + +Important `.env` values: + +```env +PORT=3000 +API_TOKEN="long-random-token" +ALLOWED_MANAGEMENT_IPS="management-server-ip" +``` + +If `ALLOWED_MANAGEMENT_IPS` is set, the agent only accepts requests from those comma-separated IP addresses. + +Install systemd service: + +```bash +sudo cp deploy/systemd/incus-backup-agent.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now incus-backup-agent +sudo journalctl -u incus-backup-agent -f +``` + +## Management API + +Run this on the management server: + +```bash +cd /opt/incus-backup-ui/management +cp .env.example .env +npm install +npm start +``` + +Important `.env` values: + +```env +PORT=3100 +SESSION_SECRET="long-random-secret" +AUTH_USERNAME="admin" +AUTH_PASSWORD="initial-password" +DATABASE_PATH="./management.sqlite" +``` + +Reset an existing password: + +```bash +npm run reset-password -- admin "new-password" +``` + +Install systemd service: + +```bash +sudo useradd --system --home /opt/incus-backup-ui --shell /usr/sbin/nologin incus-backup +sudo chown -R incus-backup:incus-backup /opt/incus-backup-ui/management +sudo cp deploy/systemd/incus-backup-management.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now incus-backup-management +sudo journalctl -u incus-backup-management -f +``` + +## Frontend + +Point the frontend at the management API: + +```bash +cd /opt/incus-backup-ui/frontend +npm install +VITE_API_URL=http://management-server:3100/api npm run build +npm run preview -- --host 0.0.0.0 +``` + +For production, put the frontend and management API behind HTTPS. diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 928ad93..57d835c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,9 +1,10 @@ import { useEffect, useMemo, useState } from 'react'; -import { AlertTriangle, CalendarClock, DatabaseBackup, LogOut, Network, RefreshCw, Settings as SettingsIcon } from 'lucide-react'; +import { AlertTriangle, CalendarClock, ClipboardList, DatabaseBackup, LogOut, Network, RefreshCw, Settings as SettingsIcon } from 'lucide-react'; import { api, errorMessage } from './api.js'; import { Dashboard } from './components/Dashboard.jsx'; import { Login } from './components/Login.jsx'; import { Nodes } from './components/Nodes.jsx'; +import { Operations } from './components/Operations.jsx'; import { Scheduler } from './components/Scheduler.jsx'; import { Settings } from './components/Settings.jsx'; import { VMDetail } from './components/VMDetail.jsx'; @@ -139,6 +140,17 @@ export default function App() { Scheduler +