diff --git a/app/client/modules/auth/routes/download-recovery-key.tsx b/app/client/modules/auth/routes/download-recovery-key.tsx
index 6ca7f0c7..7eef7eb5 100644
--- a/app/client/modules/auth/routes/download-recovery-key.tsx
+++ b/app/client/modules/auth/routes/download-recovery-key.tsx
@@ -59,9 +59,9 @@ export function DownloadRecoveryKeyPage() {
Important: Save This File Securely
- 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.
diff --git a/app/routes/(dashboard)/route.tsx b/app/routes/(dashboard)/route.tsx
index 0c26a03c..1e845539 100644
--- a/app/routes/(dashboard)/route.tsx
+++ b/app/routes/(dashboard)/route.tsx
@@ -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;
},
});
diff --git a/app/server/modules/lifecycle/migrations.ts b/app/server/modules/lifecycle/migrations.ts
index 9a4701c9..155ab989 100644
--- a/app/server/modules/lifecycle/migrations.ts
+++ b/app/server/modules/lifecycle/migrations.ts
@@ -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 => {
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`count(*)` }).from(usersTable);
diff --git a/app/server/modules/lifecycle/migrations/00007-require-recovery-key-redownload.ts b/app/server/modules/lifecycle/migrations/00007-require-recovery-key-redownload.ts
new file mode 100644
index 00000000..d277f84b
--- /dev/null
+++ b/app/server/modules/lifecycle/migrations/00007-require-recovery-key-redownload.ts
@@ -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,
+};