fix: force recovery key redownload after possible truncated download

This commit is contained in:
Nicolas Meienberger
2026-05-18 21:35:53 +02:00
parent 0f5a6823ec
commit 66ebc249ca
4 changed files with 59 additions and 6 deletions

View File

@@ -59,9 +59,9 @@ export function DownloadRecoveryKeyPage() {
<AlertTriangle className="size-5" />
<AlertTitle>Important: Save This File Securely</AlertTitle>
<AlertDescription>
Your Restic password is essential for recovering your backup data. If you lose access to this server
without this file, your backups will be unrecoverable. Store it in a password manager or encrypted
storage.
Your Restic password is essential for recovering your backup data. If you previously downloaded this
file, replace that saved copy with the new download. If you lose access to this server without this
file, your backups will be unrecoverable. Store it in a password manager or encrypted storage.
</AlertDescription>
</Alert>

View File

@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { getCookie, getRequestHeaders } from "@tanstack/react-start/server";
import { Layout } from "~/client/components/layout";
@@ -39,6 +39,10 @@ export const Route = createFileRoute("/(dashboard)")({
}),
]);
if (authContext.user && !authContext.user.hasDownloadedResticPassword) {
throw redirect({ to: "/download-recovery-key" });
}
return authContext;
},
});

View File

@@ -5,6 +5,7 @@ import { v00003 } from "./migrations/00003-assign-organization";
import { v00004 } from "./migrations/00004-concat-path-name";
import { v00005 } from "./migrations/00005-split-backup-include-paths";
import { v00006 } from "./migrations/00006-map-smb-files-to-container-uid-gid";
import { v00007 } from "./migrations/00007-require-recovery-key-redownload";
import { sql } from "drizzle-orm";
import { appMetadataTable, usersTable } from "../../db/schema";
import { db } from "../../db/db";
@@ -17,7 +18,12 @@ const recordMigrationCheckpoint = async (version: string): Promise<void> => {
await db
.insert(appMetadataTable)
.values({ key, value: JSON.stringify({ completedAt: new Date().toISOString() }), createdAt: now, updatedAt: now })
.values({
key,
value: JSON.stringify({ completedAt: new Date().toISOString() }),
createdAt: now,
updatedAt: now,
})
.onConflictDoUpdate({
target: appMetadataTable.key,
set: { value: JSON.stringify({ completedAt: new Date().toISOString() }), updatedAt: now },
@@ -39,7 +45,7 @@ type MigrationEntity = {
dependsOn?: string[];
};
const registry: MigrationEntity[] = [v00001, v00002, v00003, v00004, v00005, v00006];
const registry: MigrationEntity[] = [v00001, v00002, v00003, v00004, v00005, v00006, v00007];
export const runMigrations = async () => {
const userCount = await db.select({ count: sql<number>`count(*)` }).from(usersTable);

View File

@@ -0,0 +1,43 @@
import { and, eq, inArray } from "drizzle-orm";
import { logger } from "@zerobyte/core/node";
import { db } from "../../../db/db";
import { account, member, usersTable } from "../../../db/schema";
import { toMessage } from "~/server/utils/errors";
const execute = async () => {
const errors: Array<{ name: string; error: string }> = [];
try {
const affectedUsers = await db
.select({ id: usersTable.id })
.from(usersTable)
.innerJoin(account, and(eq(account.userId, usersTable.id), eq(account.providerId, "credential")))
.innerJoin(member, and(eq(member.userId, usersTable.id), inArray(member.role, ["owner", "admin"])))
.where(eq(usersTable.hasDownloadedResticPassword, true));
const affectedUserIds = [...new Set(affectedUsers.map((user) => user.id))];
if (affectedUserIds.length > 0) {
await db
.update(usersTable)
.set({ hasDownloadedResticPassword: false })
.where(inArray(usersTable.id, affectedUserIds));
}
logger.info(
`Migration 00007-require-recovery-key-redownload marked ${affectedUserIds.length} users for recovery key re-download.`,
);
} catch (error) {
errors.push({
name: "recovery-key-redownload",
error: toMessage(error),
});
}
return { success: errors.length === 0, errors };
};
export const v00007 = {
execute,
id: "00007-require-recovery-key-redownload",
type: "maintenance" as const,
};