69 lines
2.8 KiB
TypeScript
69 lines
2.8 KiB
TypeScript
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 } from '$lib/server/db';
|
|
import { rotateDestinationPassword } from '$lib/server/backups';
|
|
|
|
/**
|
|
* Rotate the restic repository password for a destination.
|
|
*
|
|
* Body: { currentPassword: string, newPassword: string }
|
|
*
|
|
* 200 — rotated, DB updated
|
|
* 400 — input invalid or current password wrong
|
|
* 404 — destination not found
|
|
* 409 — restic rotated but DB write failed (manual recovery needed; details
|
|
* include dbOutOfSync: true)
|
|
* 500 — restic call failed for an unrelated reason
|
|
*/
|
|
export const POST: RequestHandler = async (event) => {
|
|
const { params, request, cookies } = event;
|
|
const auth = await authorize(cookies);
|
|
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 });
|
|
|
|
const body = await request.json().catch(() => ({}));
|
|
const currentPassword = typeof body.currentPassword === 'string' ? body.currentPassword : '';
|
|
const newPassword = typeof body.newPassword === 'string' ? body.newPassword : '';
|
|
|
|
if (!currentPassword || !newPassword) {
|
|
return json({ error: 'currentPassword and newPassword are required' }, { status: 400 });
|
|
}
|
|
|
|
const dest = await getBackupDestination(id);
|
|
if (!dest) return json({ error: 'Destination not found' }, { status: 404 });
|
|
|
|
const result = await rotateDestinationPassword(id, currentPassword, newPassword);
|
|
if (!result.ok) {
|
|
// Audit the failed attempt — useful signal for spotting wrong-password
|
|
// probes against destinations.
|
|
await auditBackupDestination(event, 'update', id, dest.name, {
|
|
action: 'rotate-key',
|
|
success: false,
|
|
error: result.error,
|
|
dbOutOfSync: result.dbOutOfSync ?? false
|
|
});
|
|
if (result.dbOutOfSync) {
|
|
return json({ error: result.error, dbOutOfSync: true }, { status: 409 });
|
|
}
|
|
// Restic reports an incorrect current password as "wrong password" or
|
|
// "no key found" — surface a clearer 400 so the UI can say the current
|
|
// password is incorrect rather than a generic failure.
|
|
if (/wrong password|no key found/i.test(result.error)) {
|
|
return json({ error: 'Current password incorrect' }, { status: 400 });
|
|
}
|
|
if (result.error.toLowerCase().includes('does not match') || result.error.toLowerCase().includes('must')) {
|
|
return json({ error: result.error }, { status: 400 });
|
|
}
|
|
return json({ error: result.error }, { status: 500 });
|
|
}
|
|
|
|
await auditBackupDestination(event, 'update', id, dest.name, { action: 'rotate-key', success: true });
|
|
return json({ success: true });
|
|
};
|