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";
|
} 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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue