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
130 lines
4.6 KiB
JavaScript
130 lines
4.6 KiB
JavaScript
import dotenv from 'dotenv';
|
|
import { readFile, rename, writeFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
dotenv.config();
|
|
|
|
const envPath = path.resolve(process.cwd(), '.env');
|
|
|
|
export const requiredEnv = [
|
|
'AWS_ACCESS_KEY_ID',
|
|
'AWS_SECRET_ACCESS_KEY',
|
|
'RESTIC_REPOSITORY',
|
|
'RESTIC_PASSWORD',
|
|
'ZFS_POOL_NAME',
|
|
];
|
|
|
|
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 || '',
|
|
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY || '',
|
|
RESTIC_REPOSITORY: process.env.RESTIC_REPOSITORY || '',
|
|
RESTIC_PASSWORD: process.env.RESTIC_PASSWORD || '',
|
|
},
|
|
retention: {
|
|
keepHourly: Number(process.env.RESTIC_KEEP_HOURLY || 0),
|
|
keepDaily: Number(process.env.RESTIC_KEEP_DAILY || 7),
|
|
keepWeekly: Number(process.env.RESTIC_KEEP_WEEKLY || 0),
|
|
keepMonthly: Number(process.env.RESTIC_KEEP_MONTHLY || 0),
|
|
},
|
|
};
|
|
|
|
export const editableEnv = [
|
|
{ key: 'AWS_ACCESS_KEY_ID', label: 'AWS access key ID', required: true, secret: true },
|
|
{ key: 'AWS_SECRET_ACCESS_KEY', label: 'AWS secret access key', required: true, secret: true },
|
|
{ key: 'RESTIC_REPOSITORY', label: 'Restic repository', required: true, secret: false },
|
|
{ key: 'RESTIC_PASSWORD', label: 'Restic password', required: true, secret: true },
|
|
{ key: 'ZFS_POOL_NAME', label: 'ZFS pool name', required: true, secret: false },
|
|
{ key: 'RESTIC_KEEP_HOURLY', label: 'Keep hourly snapshots', required: false, secret: false },
|
|
{ key: 'RESTIC_KEEP_DAILY', label: 'Keep daily snapshots', required: false, secret: false },
|
|
{ key: 'RESTIC_KEEP_WEEKLY', label: 'Keep weekly snapshots', required: false, secret: false },
|
|
{ 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() {
|
|
return requiredEnv.filter((key) => !process.env[key]);
|
|
}
|
|
|
|
export function resticProcessEnv() {
|
|
return {
|
|
...process.env,
|
|
...config.resticEnv,
|
|
};
|
|
}
|
|
|
|
export async function readEnvSettings() {
|
|
const fileValues = await readEnvFile();
|
|
return editableEnv.map((field) => ({
|
|
...field,
|
|
value: fileValues[field.key] ?? process.env[field.key] ?? '',
|
|
}));
|
|
}
|
|
|
|
export async function writeEnvSettings(values) {
|
|
const allowedKeys = new Set(editableEnv.map((field) => field.key));
|
|
const currentValues = await readEnvFile();
|
|
const nextValues = { ...currentValues };
|
|
|
|
for (const [key, value] of Object.entries(values || {})) {
|
|
if (!allowedKeys.has(key)) continue;
|
|
nextValues[key] = String(value ?? '');
|
|
}
|
|
|
|
const body = editableEnv
|
|
.map(({ key }) => `${key}=${quoteEnvValue(nextValues[key] || '')}`)
|
|
.join('\n');
|
|
|
|
const tempPath = `${envPath}.tmp`;
|
|
await writeFile(tempPath, `${body}\n`, { mode: 0o600 });
|
|
await rename(tempPath, envPath);
|
|
|
|
applyRuntimeEnv(nextValues);
|
|
return readEnvSettings();
|
|
}
|
|
|
|
async function readEnvFile() {
|
|
try {
|
|
const content = await readFile(envPath, 'utf8');
|
|
return dotenv.parse(content);
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') return {};
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function quoteEnvValue(value) {
|
|
const escaped = String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
return `"${escaped}"`;
|
|
}
|
|
|
|
function applyRuntimeEnv(values) {
|
|
for (const [key, value] of Object.entries(values)) {
|
|
process.env[key] = value;
|
|
}
|
|
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 || '';
|
|
config.resticEnv.RESTIC_REPOSITORY = process.env.RESTIC_REPOSITORY || '';
|
|
config.resticEnv.RESTIC_PASSWORD = process.env.RESTIC_PASSWORD || '';
|
|
config.retention.keepHourly = Number(process.env.RESTIC_KEEP_HOURLY || 0);
|
|
config.retention.keepDaily = Number(process.env.RESTIC_KEEP_DAILY || 7);
|
|
config.retention.keepWeekly = Number(process.env.RESTIC_KEEP_WEEKLY || 0);
|
|
config.retention.keepMonthly = Number(process.env.RESTIC_KEEP_MONTHLY || 0);
|
|
}
|