S3 backup destinations take an AWS region with a region quick-pick

This commit is contained in:
jarek committed 2026-08-09 10:31:27 +02:00
1 parent 3d1b383ad7
commit 019bea222e
4 files changed
+315 -94

No files matched your search

+17 -10
View File
@@ -1,6 +1,7 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { authorize } from '$lib/server/authorize';
import { requireBackups } from '$lib/server/backups/route-guards';
import { auditBackupDestination } from '$lib/server/audit';
import {
getBackupDestinations,
@@ -11,7 +12,7 @@ import {
} from '$lib/server/db';
import { initRepository, testRepository } from '$lib/server/backups';
import { registerSchedule } from '$lib/server/scheduler';
import { validateRepositoryForSave, validateFlags, validatePolicySchedules } from '$lib/server/backups/helpers';
import { validateRepositoryForSave, validateAndSerializeFlags, validatePolicySchedules } from '$lib/server/backups/helpers';
/**
* Prepare destination for API response — strip password, parse env vars.
@@ -37,9 +38,8 @@ function prepareDestination(dest: any, opts: { includeEnvVars: boolean }): any {
export const GET: RequestHandler = async ({ cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('backups', 'view')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const denied = await requireBackups(auth, 'view');
if (denied) return denied;
const destinations = await getBackupDestinations();
// LIST: strip envVars (cloud creds). Modal re-fetches single destination to edit.
@@ -49,9 +49,8 @@ export const GET: RequestHandler = async ({ cookies }) => {
export const POST: RequestHandler = async (event) => {
const { request, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('backups', 'manage')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const denied = await requireBackups(auth, 'manage');
if (denied) return denied;
const body = await request.json();
@@ -63,8 +62,16 @@ export const POST: RequestHandler = async (event) => {
// BEFORE persisting, so nothing is saved on bad input.
const repoError = validateRepositoryForSave(body.repository);
if (repoError) return json({ error: repoError }, { status: 400 });
const flagError = validateFlags(body.flags);
if (flagError) return json({ error: flagError }, { status: 400 });
// Flags: prefer the split shape (backupFlags/restoreFlags); fall back to a legacy `flags`
// string (treated as backup flags). Validate+serialize to the JSON stored in `flags`.
let flagsColumn: string | null = null;
try {
flagsColumn = (body.backupFlags !== undefined || body.restoreFlags !== undefined)
? validateAndSerializeFlags(body.backupFlags, body.restoreFlags)
: validateAndSerializeFlags(body.flags, '');
} catch (e) {
return json({ error: e instanceof Error ? e.message : 'Invalid restic flags' }, { status: 400 });
}
// Validate any cron schedules in the supplied policies (audit #7)
const policyCronError = validatePolicySchedules(body.policies);
@@ -85,7 +92,7 @@ export const POST: RequestHandler = async (event) => {
repository: body.repository,
password: body.password,
envVars: body.envVars ? JSON.stringify(body.envVars) : null,
flags: body.flags ?? null,
flags: flagsColumn,
hostPath: body.hostPath ?? null,
policies: body.policies ?? defaultPolicies
});
@@ -1,6 +1,7 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { authorize } from '$lib/server/authorize';
import { requireBackups } from '$lib/server/backups/route-guards';
import { auditBackupDestination } from '$lib/server/audit';
import {
getBackupDestination,
@@ -10,7 +11,7 @@ import {
getBackupConfigs
} from '$lib/server/db';
import { registerSchedule, unregisterSchedule } from '$lib/server/scheduler';
import { validatePolicySchedules, validateRepositoryForSave, validateFlags } from '$lib/server/backups/helpers';
import { validatePolicySchedules, validateRepositoryForSave, validateAndSerializeFlags, parseBackupFlags } from '$lib/server/backups/helpers';
import { destinationHasRunningBackup } from '$lib/server/backups';
/**
@@ -28,14 +29,18 @@ function prepareDestination(dest: any, includeSecrets: boolean): any {
} else {
delete result.envVars;
}
// Split the stored `flags` JSON into separate fields the edit form binds to (legacy
// bare strings surface as backupFlags), so the UI never parses the string-vs-JSON column.
const { backup, restore } = parseBackupFlags(dest.flags);
result.backupFlags = backup;
result.restoreFlags = restore;
return result;
}
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('backups', 'view')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const denied = await requireBackups(auth, 'view');
if (denied) return denied;
const id = parseInt(params.id);
if (isNaN(id)) return json({ error: 'Invalid ID' }, { status: 400 });
@@ -52,9 +57,8 @@ export const GET: RequestHandler = async ({ params, cookies }) => {
export const PUT: RequestHandler = async (event) => {
const { params, request, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('backups', 'manage')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const denied = await requireBackups(auth, 'manage');
if (denied) return denied;
const id = parseInt(params.id);
if (isNaN(id)) return json({ error: 'Invalid ID' }, { status: 400 });
@@ -82,8 +86,16 @@ export const PUT: RequestHandler = async (event) => {
const repoError = validateRepositoryForSave(body.repository);
if (repoError) return json({ error: repoError }, { status: 400 });
}
const flagError = validateFlags(body.flags);
if (flagError) return json({ error: flagError }, { status: 400 });
// Flags: prefer the split shape; fall back to a legacy `flags` string. undefined for ALL
// three means "don't touch flags". Validate+serialize to the JSON stored in `flags`.
let flagsColumn: string | null | undefined = undefined;
if (body.backupFlags !== undefined || body.restoreFlags !== undefined) {
try { flagsColumn = validateAndSerializeFlags(body.backupFlags, body.restoreFlags); }
catch (e) { return json({ error: e instanceof Error ? e.message : 'Invalid restic flags' }, { status: 400 }); }
} else if (body.flags !== undefined) {
try { flagsColumn = validateAndSerializeFlags(body.flags, ''); }
catch (e) { return json({ error: e instanceof Error ? e.message : 'Invalid restic flags' }, { status: 400 }); }
}
// A local-path repo is allowed on any env; a wrong-host mount fails loud via
// the helper's localRepoGuard at backup/restore time.
@@ -94,7 +106,7 @@ export const PUT: RequestHandler = async (event) => {
repository: body.repository,
password: body.password,
envVars: body.envVars !== undefined ? JSON.stringify(body.envVars) : undefined,
flags: body.flags,
flags: flagsColumn,
hostPath: body.hostPath,
policies: body.policies
});
@@ -131,9 +143,8 @@ export const PUT: RequestHandler = async (event) => {
export const DELETE: RequestHandler = async (event) => {
const { params, cookies } = event;
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('backups', 'manage')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const denied = await requireBackups(auth, 'manage');
if (denied) return denied;
const id = parseInt(params.id);
if (isNaN(id)) return json({ error: 'Invalid ID' }, { status: 400 });