refactor: clean up imports and improve formatting in export dialog and settings components

This commit is contained in:
Jakub Trávník 2025-12-30 00:06:49 +01:00
parent e15fff2c23
commit ce714f7bfc
3 changed files with 48 additions and 55 deletions

View file

@ -16,13 +16,7 @@ import {
} from "~/client/components/ui/dialog"; } from "~/client/components/ui/dialog";
import { Input } from "~/client/components/ui/input"; import { Input } from "~/client/components/ui/input";
import { Label } from "~/client/components/ui/label"; import { Label } from "~/client/components/ui/label";
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/client/components/ui/select";
type SecretsMode = "exclude" | "encrypted" | "cleartext"; type SecretsMode = "exclude" | "encrypted" | "cleartext";
@ -88,9 +82,7 @@ export function ExportDialog({
const handleExportError = (error: unknown) => { const handleExportError = (error: unknown) => {
const message = const message =
error && typeof error === "object" && "error" in error error && typeof error === "object" && "error" in error ? (error as { error: string }).error : "Unknown error";
? (error as { error: string }).error
: "Unknown error";
toast.error("Export failed", { toast.error("Export failed", {
description: message, 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" 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"
> >
<Download className="h-8 w-8 text-muted-foreground" /> <Download className="h-8 w-8 text-muted-foreground" />
<span className="text-sm font-medium text-muted-foreground"> <span className="text-sm font-medium text-muted-foreground">{triggerLabel ?? "Export Config"}</span>
{triggerLabel ?? "Export Config"}
</span>
</button> </button>
) : ( ) : (
<Button variant={variant} size={size}> <Button variant={variant} size={size}>
@ -152,9 +142,7 @@ export function ExportDialog({
<form onSubmit={handleExport}> <form onSubmit={handleExport}>
<DialogHeader> <DialogHeader>
<DialogTitle>Export Full Configuration</DialogTitle> <DialogTitle>Export Full Configuration</DialogTitle>
<DialogDescription> <DialogDescription>Export the complete Zerobyte configuration.</DialogDescription>
Export the complete Zerobyte configuration.
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
@ -169,7 +157,7 @@ export function ExportDialog({
</Label> </Label>
</div> </div>
<p className="text-xs text-muted-foreground ml-7"> <p className="text-xs text-muted-foreground ml-7">
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).
</p> </p>
<div className="flex items-center justify-between pt-2 border-t"> <div className="flex items-center justify-between pt-2 border-t">
@ -208,9 +196,8 @@ export function ExportDialog({
</Label> </Label>
</div> </div>
<p className="text-xs text-muted-foreground ml-7"> <p className="text-xs text-muted-foreground ml-7">
<span className="text-yellow-600 font-medium"> Security sensitive:</span> The recovery <span className="text-yellow-600 font-medium"> Security sensitive:</span> The recovery key is the master
key is the master encryption key for all repositories. Keep this export secure and never encryption key for all repositories. Keep this export secure and never share it.
share it.
</p> </p>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
@ -224,8 +211,8 @@ export function ExportDialog({
</Label> </Label>
</div> </div>
<p className="text-xs text-muted-foreground ml-7"> <p className="text-xs text-muted-foreground ml-7">
Include the hashed user passwords for seamless migration. The passwords are already securely Include the hashed user passwords for seamless migration. The passwords are already securely hashed
hashed (argon2). (argon2).
</p> </p>
<div className="space-y-2 pt-2 border-t"> <div className="space-y-2 pt-2 border-t">

View file

@ -276,8 +276,8 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
</div> </div>
<CardContent className="p-6 space-y-4"> <CardContent className="p-6 space-y-4">
<p className="text-sm text-muted-foreground max-w-2xl"> <p className="text-sm text-muted-foreground max-w-2xl">
Export all your volumes, repositories, backup schedules, and notification settings to a JSON file. Export all your volumes, repositories, backup schedules, and notification settings to a JSON file. This can be
This can be used to restore your configuration on a new instance or as a backup of your settings. used to restore your configuration on a new instance or as a backup of your settings.
</p> </p>
<ExportDialog triggerLabel="Export Configuration" /> <ExportDialog triggerLabel="Export Configuration" />
</CardContent> </CardContent>

View file

@ -1,7 +1,12 @@
import { validator } from "hono-openapi"; import { validator } from "hono-openapi";
import { Hono } from "hono"; import { Hono } from "hono";
import type { Context } 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 { db } from "../../db/db";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { RESTIC_PASS_FILE } from "../../core/constants"; import { RESTIC_PASS_FILE } from "../../core/constants";
@ -11,12 +16,7 @@ import { volumeService } from "../volumes/volume.service";
import { repositoriesService } from "../repositories/repositories.service"; import { repositoriesService } from "../repositories/repositories.service";
import { notificationsService } from "../notifications/notifications.service"; import { notificationsService } from "../notifications/notifications.service";
import { backupsService } from "../backups/backups.service"; import { backupsService } from "../backups/backups.service";
import { import { fullExportBodySchema, fullExportDto, type SecretsMode, type FullExportBody } from "./config-export.dto";
fullExportBodySchema,
fullExportDto,
type SecretsMode,
type FullExportBody,
} from "./config-export.dto";
import { requireAuth } from "../auth/auth.middleware"; import { requireAuth } from "../auth/auth.middleware";
type ExportParams = { type ExportParams = {
@ -27,8 +27,25 @@ type ExportParams = {
// Keys to exclude when metadata is not included // Keys to exclude when metadata is not included
const METADATA_KEYS = { const METADATA_KEYS = {
ids: ["id", "shortId", "volumeId", "repositoryId", "scheduleId", "destinationId"], ids: ["id", "shortId", "volumeId", "repositoryId", "scheduleId", "destinationId"],
timestamps: ["createdAt", "updatedAt", "lastBackupAt", "nextBackupAt", "lastHealthCheck", "lastChecked", "lastCopyAt"], timestamps: [
runtimeState: ["status", "lastError", "lastBackupStatus", "lastBackupError", "hasDownloadedResticPassword", "lastCopyStatus", "lastCopyError", "sortOrder"], "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]; const ALL_METADATA_KEYS = [...METADATA_KEYS.ids, ...METADATA_KEYS.timestamps, ...METADATA_KEYS.runtimeState];
@ -46,10 +63,7 @@ function filterMetadataOut<T extends Record<string, unknown>>(obj: T, includeMet
} }
/** Parse export params from request body */ /** Parse export params from request body */
function parseExportParamsFromBody(body: { function parseExportParamsFromBody(body: { includeMetadata?: boolean; secretsMode?: SecretsMode }): ExportParams {
includeMetadata?: boolean;
secretsMode?: SecretsMode;
}): ExportParams {
const includeMetadata = body.includeMetadata === true; const includeMetadata = body.includeMetadata === true;
const secretsMode: SecretsMode = body.secretsMode ?? "exclude"; const secretsMode: SecretsMode = body.secretsMode ?? "exclude";
return { includeMetadata, secretsMode }; return { includeMetadata, secretsMode };
@ -61,7 +75,7 @@ function parseExportParamsFromBody(body: {
*/ */
async function verifyExportPassword( async function verifyExportPassword(
c: Context, c: Context,
password: string password: string,
): Promise<{ valid: true; userId: number } | { valid: false; error: string }> { ): Promise<{ valid: true; userId: number } | { valid: false; error: string }> {
// requireAuth middleware ensures c.get('user') exists // requireAuth middleware ensures c.get('user') exists
const user = c.get("user"); const user = c.get("user");
@ -83,7 +97,7 @@ async function verifyExportPassword(
*/ */
async function processSecrets( async function processSecrets(
obj: Record<string, unknown>, obj: Record<string, unknown>,
secretsMode: SecretsMode secretsMode: SecretsMode,
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
if (secretsMode === "encrypted") { if (secretsMode === "encrypted") {
return obj; return obj;
@ -108,8 +122,8 @@ async function processSecrets(
value.map(async (item) => value.map(async (item) =>
item && typeof item === "object" && !Array.isArray(item) item && typeof item === "object" && !Array.isArray(item)
? processSecrets(item as Record<string, unknown>, secretsMode) ? processSecrets(item as Record<string, unknown>, secretsMode)
: item : item,
) ),
); );
} else if (value && typeof value === "object") { } else if (value && typeof value === "object") {
result[key] = await processSecrets(value as Record<string, unknown>, secretsMode); result[key] = await processSecrets(value as Record<string, unknown>, secretsMode);
@ -120,10 +134,7 @@ async function processSecrets(
} }
/** Clean and process an entity for export */ /** Clean and process an entity for export */
async function exportEntity( async function exportEntity(entity: Record<string, unknown>, params: ExportParams): Promise<Record<string, unknown>> {
entity: Record<string, unknown>,
params: ExportParams
): Promise<Record<string, unknown>> {
const cleaned = filterMetadataOut(entity, params.includeMetadata); const cleaned = filterMetadataOut(entity, params.includeMetadata);
return processSecrets(cleaned, params.secretsMode); return processSecrets(cleaned, params.secretsMode);
} }
@ -131,7 +142,7 @@ async function exportEntity(
/** Export multiple entities */ /** Export multiple entities */
async function exportEntities<T extends Record<string, unknown>>( async function exportEntities<T extends Record<string, unknown>>(
entities: T[], entities: T[],
params: ExportParams params: ExportParams,
): Promise<Record<string, unknown>[]> { ): Promise<Record<string, unknown>[]> {
return Promise.all(entities.map((e) => exportEntity(e as Record<string, unknown>, params))); return Promise.all(entities.map((e) => exportEntity(e as Record<string, unknown>, params)));
} }
@ -144,7 +155,7 @@ function transformBackupSchedules(
volumeMap: Map<number, string>, volumeMap: Map<number, string>,
repoMap: Map<string, string>, repoMap: Map<string, string>,
notificationMap: Map<number, string>, notificationMap: Map<number, string>,
params: ExportParams params: ExportParams,
) { ) {
return schedules.map((schedule) => { return schedules.map((schedule) => {
const assignments = scheduleNotifications const assignments = scheduleNotifications
@ -173,11 +184,7 @@ function transformBackupSchedules(
export const configExportController = new Hono() export const configExportController = new Hono()
.use(requireAuth) .use(requireAuth)
.post( .post("/export", fullExportDto, validator("json", fullExportBodySchema), async (c) => {
"/export",
fullExportDto,
validator("json", fullExportBodySchema),
async (c) => {
try { try {
const body = c.req.valid("json") as FullExportBody; const body = c.req.valid("json") as FullExportBody;
@ -214,7 +221,7 @@ export const configExportController = new Hono()
volumeMap, volumeMap,
repoMap, repoMap,
notificationMap, notificationMap,
params params,
); );
const [exportVolumes, exportRepositories, exportNotifications] = await Promise.all([ 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)}`); 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); return c.json({ error: err instanceof Error ? err.message : "Failed to export config" }, 500);
} }
} });
);