zerobyte/app/server/modules/lifecycle/config-export.controller.ts
2026-01-04 14:18:10 +01:00

155 lines
4.4 KiB
TypeScript

import { validator } from "hono-openapi";
import { Hono } from "hono";
import type { Context } from "hono";
import {
backupScheduleNotificationsTable,
backupScheduleMirrorsTable,
usersTable,
type BackupSchedule,
type BackupScheduleNotification,
type BackupScheduleMirror,
volumesTable,
repositoriesTable,
backupSchedulesTable,
notificationDestinationsTable,
} from "../../db/schema";
import { db } from "../../db/db";
import { logger } from "../../utils/logger";
import { authService } from "../auth/auth.service";
import { fullExportBodySchema, fullExportDto, type FullExportBody } from "./config-export.dto";
import { requireAuth } from "../auth/auth.middleware";
import { toMessage } from "~/server/utils/errors";
const METADATA_KEYS = {
timestamps: [
"createdAt",
"updatedAt",
"lastBackupAt",
"nextBackupAt",
"lastHealthCheck",
"lastChecked",
"lastCopyAt",
],
runtimeState: [
"status",
"lastError",
"lastBackupStatus",
"lastBackupError",
"hasDownloadedResticPassword",
"lastCopyStatus",
"lastCopyError",
"sortOrder",
],
};
const ALL_METADATA_KEYS = [...METADATA_KEYS.timestamps, ...METADATA_KEYS.runtimeState];
function filterMetadataOut<T extends Record<string, unknown>>(obj: T, includeMetadata: boolean): Partial<T> {
if (includeMetadata) {
return obj;
}
const result = { ...obj };
for (const key of ALL_METADATA_KEYS) {
delete result[key as keyof T];
}
return result;
}
async function verifyExportPassword(
c: Context,
password: string,
): Promise<{ valid: true; userId: number } | { valid: false; error: string }> {
const user = c.get("user");
if (!user) {
return { valid: false, error: "Not authenticated" };
}
const isValid = await authService.verifyPassword(user.id, password);
if (!isValid) {
return { valid: false, error: "Incorrect password" };
}
return { valid: true, userId: user.id };
}
async function exportEntity(entity: Record<string, unknown>, params: FullExportBody) {
return filterMetadataOut(entity, params.includeMetadata);
}
async function exportEntities<T extends Record<string, unknown>>(entities: T[], params: FullExportBody) {
return Promise.all(entities.map((e) => exportEntity(e, params)));
}
const transformBackupSchedules = (
schedules: BackupSchedule[],
scheduleNotifications: BackupScheduleNotification[],
scheduleMirrors: BackupScheduleMirror[],
params: FullExportBody,
) => {
return schedules.map((schedule) => {
const assignments = scheduleNotifications
.filter((sn) => sn.scheduleId === schedule.id)
.map((sn) => filterMetadataOut(sn, params.includeMetadata));
const mirrors = scheduleMirrors
.filter((sm) => sm.scheduleId === schedule.id)
.map((sm) => filterMetadataOut(sm, params.includeMetadata));
return {
...filterMetadataOut(schedule, params.includeMetadata),
notifications: assignments,
mirrors,
};
});
};
export const configExportController = new Hono()
.use(requireAuth)
.post("/export", fullExportDto, validator("json", fullExportBodySchema), async (c) => {
try {
const params = c.req.valid("json");
const verification = await verifyExportPassword(c, params.password);
if (!verification.valid) {
return c.json({ error: verification.error }, 401);
}
const [volumes, repositories, backupSchedulesRaw, notifications, scheduleNotifications, scheduleMirrors, users] =
await Promise.all([
db.select().from(volumesTable),
db.select().from(repositoriesTable),
db.select().from(backupSchedulesTable),
db.select().from(notificationDestinationsTable),
db.select().from(backupScheduleNotificationsTable),
db.select().from(backupScheduleMirrorsTable),
db.select().from(usersTable),
]);
const backupSchedules = transformBackupSchedules(
backupSchedulesRaw,
scheduleNotifications,
scheduleMirrors,
params,
);
const [exportVolumes, exportRepositories, exportNotifications, exportUsers] = await Promise.all([
exportEntities(volumes, params),
exportEntities(repositories, params),
exportEntities(notifications, params),
exportEntities(users, params),
]);
return c.json({
version: 1,
exportedAt: new Date().toISOString(),
volumes: exportVolumes,
repositories: exportRepositories,
backupSchedules,
notificationDestinations: exportNotifications,
users: exportUsers,
});
} catch (err) {
logger.error(`Config export failed: ${toMessage(err)}`);
return c.json({ error: toMessage(err) }, 500);
}
});