S3 backup destinations take an AWS region with a region quick-pick
This commit is contained in:
4 files changed
+315
-94
No files matched your search
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user