diff --git a/app/client/components/export-dialog.tsx b/app/client/components/export-dialog.tsx index 00db200b..89dd2ef5 100644 --- a/app/client/components/export-dialog.tsx +++ b/app/client/components/export-dialog.tsx @@ -16,13 +16,7 @@ import { } from "~/client/components/ui/dialog"; import { Input } from "~/client/components/ui/input"; import { Label } from "~/client/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "~/client/components/ui/select"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; type SecretsMode = "exclude" | "encrypted" | "cleartext"; @@ -88,9 +82,7 @@ export function ExportDialog({ const handleExportError = (error: unknown) => { const message = - error && typeof error === "object" && "error" in error - ? (error as { error: string }).error - : "Unknown error"; + error && typeof error === "object" && "error" in error ? (error as { error: string }).error : "Unknown error"; toast.error("Export failed", { description: message, }); @@ -134,9 +126,7 @@ export function ExportDialog({ className="flex flex-col items-center justify-center gap-2 cursor-pointer h-full w-full border-0 bg-transparent p-0 hover:opacity-80 transition-opacity" > - - {triggerLabel ?? "Export Config"} - + {triggerLabel ?? "Export Config"} ) : ( @@ -152,9 +142,7 @@ export function ExportDialog({ Export Full Configuration - - Export the complete Zerobyte configuration. - + Export the complete Zerobyte configuration. @@ -169,7 +157,7 @@ export function ExportDialog({ - Include database IDs, timestamps, and runtime state (status, health checks, last backup info). + Include database IDs, timestamps, and runtime state (status, health checks, last backup info). @@ -208,9 +196,8 @@ export function ExportDialog({ - ⚠️ Security sensitive: The recovery - key is the master encryption key for all repositories. Keep this export secure and never - share it. + ⚠️ Security sensitive: The recovery key is the master + encryption key for all repositories. Keep this export secure and never share it. @@ -224,8 +211,8 @@ export function ExportDialog({ - Include the hashed user passwords for seamless migration. The passwords are already securely - hashed (argon2). + Include the hashed user passwords for seamless migration. The passwords are already securely hashed + (argon2). diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx index 2999b722..d83510c2 100644 --- a/app/client/modules/settings/routes/settings.tsx +++ b/app/client/modules/settings/routes/settings.tsx @@ -276,8 +276,8 @@ export default function Settings({ loaderData }: Route.ComponentProps) { - Export all your volumes, repositories, backup schedules, and notification settings to a JSON file. - This can be used to restore your configuration on a new instance or as a backup of your settings. + Export all your volumes, repositories, backup schedules, and notification settings to a JSON file. This can be + used to restore your configuration on a new instance or as a backup of your settings. diff --git a/app/server/modules/lifecycle/config-export.controller.ts b/app/server/modules/lifecycle/config-export.controller.ts index 12b6089a..eec97684 100644 --- a/app/server/modules/lifecycle/config-export.controller.ts +++ b/app/server/modules/lifecycle/config-export.controller.ts @@ -1,7 +1,12 @@ import { validator } from "hono-openapi"; import { Hono } from "hono"; import type { Context } from "hono"; -import { backupSchedulesTable, backupScheduleNotificationsTable, backupScheduleMirrorsTable, usersTable } from "../../db/schema"; +import { + type backupSchedulesTable, + backupScheduleNotificationsTable, + backupScheduleMirrorsTable, + usersTable, +} from "../../db/schema"; import { db } from "../../db/db"; import { logger } from "../../utils/logger"; import { RESTIC_PASS_FILE } from "../../core/constants"; @@ -11,12 +16,7 @@ import { volumeService } from "../volumes/volume.service"; import { repositoriesService } from "../repositories/repositories.service"; import { notificationsService } from "../notifications/notifications.service"; import { backupsService } from "../backups/backups.service"; -import { - fullExportBodySchema, - fullExportDto, - type SecretsMode, - type FullExportBody, -} from "./config-export.dto"; +import { fullExportBodySchema, fullExportDto, type SecretsMode, type FullExportBody } from "./config-export.dto"; import { requireAuth } from "../auth/auth.middleware"; type ExportParams = { @@ -27,8 +27,25 @@ type ExportParams = { // Keys to exclude when metadata is not included const METADATA_KEYS = { ids: ["id", "shortId", "volumeId", "repositoryId", "scheduleId", "destinationId"], - timestamps: ["createdAt", "updatedAt", "lastBackupAt", "nextBackupAt", "lastHealthCheck", "lastChecked", "lastCopyAt"], - runtimeState: ["status", "lastError", "lastBackupStatus", "lastBackupError", "hasDownloadedResticPassword", "lastCopyStatus", "lastCopyError", "sortOrder"], + timestamps: [ + "createdAt", + "updatedAt", + "lastBackupAt", + "nextBackupAt", + "lastHealthCheck", + "lastChecked", + "lastCopyAt", + ], + runtimeState: [ + "status", + "lastError", + "lastBackupStatus", + "lastBackupError", + "hasDownloadedResticPassword", + "lastCopyStatus", + "lastCopyError", + "sortOrder", + ], }; const ALL_METADATA_KEYS = [...METADATA_KEYS.ids, ...METADATA_KEYS.timestamps, ...METADATA_KEYS.runtimeState]; @@ -46,10 +63,7 @@ function filterMetadataOut>(obj: T, includeMet } /** Parse export params from request body */ -function parseExportParamsFromBody(body: { - includeMetadata?: boolean; - secretsMode?: SecretsMode; -}): ExportParams { +function parseExportParamsFromBody(body: { includeMetadata?: boolean; secretsMode?: SecretsMode }): ExportParams { const includeMetadata = body.includeMetadata === true; const secretsMode: SecretsMode = body.secretsMode ?? "exclude"; return { includeMetadata, secretsMode }; @@ -61,7 +75,7 @@ function parseExportParamsFromBody(body: { */ async function verifyExportPassword( c: Context, - password: string + password: string, ): Promise<{ valid: true; userId: number } | { valid: false; error: string }> { // requireAuth middleware ensures c.get('user') exists const user = c.get("user"); @@ -83,7 +97,7 @@ async function verifyExportPassword( */ async function processSecrets( obj: Record, - secretsMode: SecretsMode + secretsMode: SecretsMode, ): Promise> { if (secretsMode === "encrypted") { return obj; @@ -108,8 +122,8 @@ async function processSecrets( value.map(async (item) => item && typeof item === "object" && !Array.isArray(item) ? processSecrets(item as Record, secretsMode) - : item - ) + : item, + ), ); } else if (value && typeof value === "object") { result[key] = await processSecrets(value as Record, secretsMode); @@ -120,10 +134,7 @@ async function processSecrets( } /** Clean and process an entity for export */ -async function exportEntity( - entity: Record, - params: ExportParams -): Promise> { +async function exportEntity(entity: Record, params: ExportParams): Promise> { const cleaned = filterMetadataOut(entity, params.includeMetadata); return processSecrets(cleaned, params.secretsMode); } @@ -131,7 +142,7 @@ async function exportEntity( /** Export multiple entities */ async function exportEntities>( entities: T[], - params: ExportParams + params: ExportParams, ): Promise[]> { return Promise.all(entities.map((e) => exportEntity(e as Record, params))); } @@ -144,7 +155,7 @@ function transformBackupSchedules( volumeMap: Map, repoMap: Map, notificationMap: Map, - params: ExportParams + params: ExportParams, ) { return schedules.map((schedule) => { const assignments = scheduleNotifications @@ -173,11 +184,7 @@ function transformBackupSchedules( export const configExportController = new Hono() .use(requireAuth) - .post( - "/export", - fullExportDto, - validator("json", fullExportBodySchema), - async (c) => { + .post("/export", fullExportDto, validator("json", fullExportBodySchema), async (c) => { try { const body = c.req.valid("json") as FullExportBody; @@ -214,7 +221,7 @@ export const configExportController = new Hono() volumeMap, repoMap, notificationMap, - params + params, ); const [exportVolumes, exportRepositories, exportNotifications] = await Promise.all([ @@ -255,5 +262,4 @@ export const configExportController = new Hono() logger.error(`Config export failed: ${err instanceof Error ? err.message : String(err)}`); return c.json({ error: err instanceof Error ? err.message : "Failed to export config" }, 500); } - } -); + });
- Include database IDs, timestamps, and runtime state (status, health checks, last backup info). + Include database IDs, timestamps, and runtime state (status, health checks, last backup info).
- ⚠️ Security sensitive: The recovery - key is the master encryption key for all repositories. Keep this export secure and never - share it. + ⚠️ Security sensitive: The recovery key is the master + encryption key for all repositories. Keep this export secure and never share it.
- Include the hashed user passwords for seamless migration. The passwords are already securely - hashed (argon2). + Include the hashed user passwords for seamless migration. The passwords are already securely hashed + (argon2).
- Export all your volumes, repositories, backup schedules, and notification settings to a JSON file. - This can be used to restore your configuration on a new instance or as a backup of your settings. + Export all your volumes, repositories, backup schedules, and notification settings to a JSON file. This can be + used to restore your configuration on a new instance or as a backup of your settings.