refactor: clean up imports and improve formatting in export dialog and settings components
This commit is contained in:
parent
e15fff2c23
commit
ce714f7bfc
3 changed files with 48 additions and 55 deletions
|
|
@ -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"
|
||||
>
|
||||
<Download className="h-8 w-8 text-muted-foreground" />
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{triggerLabel ?? "Export Config"}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-muted-foreground">{triggerLabel ?? "Export Config"}</span>
|
||||
</button>
|
||||
) : (
|
||||
<Button variant={variant} size={size}>
|
||||
|
|
@ -152,9 +142,7 @@ export function ExportDialog({
|
|||
<form onSubmit={handleExport}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Export Full Configuration</DialogTitle>
|
||||
<DialogDescription>
|
||||
Export the complete Zerobyte configuration.
|
||||
</DialogDescription>
|
||||
<DialogDescription>Export the complete Zerobyte configuration.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
|
|
@ -169,7 +157,7 @@ export function ExportDialog({
|
|||
</Label>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t">
|
||||
|
|
@ -208,9 +196,8 @@ export function ExportDialog({
|
|||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground ml-7">
|
||||
<span className="text-yellow-600 font-medium">⚠️ Security sensitive:</span> The recovery
|
||||
key is the master encryption key for all repositories. Keep this export secure and never
|
||||
share it.
|
||||
<span className="text-yellow-600 font-medium">⚠️ Security sensitive:</span> The recovery key is the master
|
||||
encryption key for all repositories. Keep this export secure and never share it.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
|
|
@ -224,8 +211,8 @@ export function ExportDialog({
|
|||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground ml-7">
|
||||
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).
|
||||
</p>
|
||||
|
||||
<div className="space-y-2 pt-2 border-t">
|
||||
|
|
|
|||
|
|
@ -276,8 +276,8 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
|||
</div>
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<p className="text-sm text-muted-foreground max-w-2xl">
|
||||
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.
|
||||
</p>
|
||||
<ExportDialog triggerLabel="Export Configuration" />
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -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<T extends Record<string, unknown>>(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<string, unknown>,
|
||||
secretsMode: SecretsMode
|
||||
secretsMode: SecretsMode,
|
||||
): Promise<Record<string, unknown>> {
|
||||
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<string, unknown>, secretsMode)
|
||||
: item
|
||||
)
|
||||
: item,
|
||||
),
|
||||
);
|
||||
} else if (value && typeof value === "object") {
|
||||
result[key] = await processSecrets(value as Record<string, unknown>, secretsMode);
|
||||
|
|
@ -120,10 +134,7 @@ async function processSecrets(
|
|||
}
|
||||
|
||||
/** Clean and process an entity for export */
|
||||
async function exportEntity(
|
||||
entity: Record<string, unknown>,
|
||||
params: ExportParams
|
||||
): Promise<Record<string, unknown>> {
|
||||
async function exportEntity(entity: Record<string, unknown>, params: ExportParams): Promise<Record<string, unknown>> {
|
||||
const cleaned = filterMetadataOut(entity, params.includeMetadata);
|
||||
return processSecrets(cleaned, params.secretsMode);
|
||||
}
|
||||
|
|
@ -131,7 +142,7 @@ async function exportEntity(
|
|||
/** Export multiple entities */
|
||||
async function exportEntities<T extends Record<string, unknown>>(
|
||||
entities: T[],
|
||||
params: ExportParams
|
||||
params: ExportParams,
|
||||
): Promise<Record<string, unknown>[]> {
|
||||
return Promise.all(entities.map((e) => exportEntity(e as Record<string, unknown>, params)));
|
||||
}
|
||||
|
|
@ -144,7 +155,7 @@ function transformBackupSchedules(
|
|||
volumeMap: Map<number, string>,
|
||||
repoMap: Map<string, string>,
|
||||
notificationMap: Map<number, string>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue