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, +};