From 52d4e452a2b9e5f6434e0dd8ea82840d3a797975 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Tue, 19 May 2026 20:10:36 +0200 Subject: [PATCH] refactor: move from session storage to cookie --- app/client/lib/recovery-key-skip.ts | 11 ----------- .../modules/auth/routes/download-recovery-key.tsx | 12 +++++++++--- app/client/modules/auth/routes/login.tsx | 12 +++++++++--- app/context.ts | 1 + app/lib/recovery-key-skip.ts | 2 ++ app/middleware/auth.ts | 11 +++++++++-- app/routes/(auth)/download-recovery-key.tsx | 5 +++-- app/routes/(dashboard)/route.tsx | 11 +++++++++-- 8 files changed, 42 insertions(+), 23 deletions(-) delete mode 100644 app/client/lib/recovery-key-skip.ts create mode 100644 app/lib/recovery-key-skip.ts diff --git a/app/client/lib/recovery-key-skip.ts b/app/client/lib/recovery-key-skip.ts deleted file mode 100644 index c51b08c4..00000000 --- a/app/client/lib/recovery-key-skip.ts +++ /dev/null @@ -1,11 +0,0 @@ -const RECOVERY_KEY_DOWNLOAD_SKIPPED_KEY = "zerobyte:recovery-key-download-skipped"; - -export function hasSkippedRecoveryKeyDownload() { - if (typeof window === "undefined") return false; - - return window.sessionStorage.getItem(RECOVERY_KEY_DOWNLOAD_SKIPPED_KEY) === "true"; -} - -export function skipRecoveryKeyDownload() { - window.sessionStorage.setItem(RECOVERY_KEY_DOWNLOAD_SKIPPED_KEY, "true"); -} diff --git a/app/client/modules/auth/routes/download-recovery-key.tsx b/app/client/modules/auth/routes/download-recovery-key.tsx index 971aa27c..842f3307 100644 --- a/app/client/modules/auth/routes/download-recovery-key.tsx +++ b/app/client/modules/auth/routes/download-recovery-key.tsx @@ -9,7 +9,10 @@ import { Input } from "~/client/components/ui/input"; import { Label } from "~/client/components/ui/label"; import { downloadResticPasswordMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { parseError } from "~/client/lib/errors"; -import { skipRecoveryKeyDownload } from "~/client/lib/recovery-key-skip"; +import { + RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_MAX_AGE, + RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME, +} from "~/lib/recovery-key-skip"; import { useNavigate } from "@tanstack/react-router"; const RECOVERY_KEY_CREDENTIAL_REQUIRED_MESSAGE = @@ -17,9 +20,10 @@ const RECOVERY_KEY_CREDENTIAL_REQUIRED_MESSAGE = type Props = { hasCredentialPassword: boolean; + userId: string | null; }; -export function DownloadRecoveryKeyPage({ hasCredentialPassword }: Props) { +export function DownloadRecoveryKeyPage({ hasCredentialPassword, userId }: Props) { const navigate = useNavigate(); const [password, setPassword] = useState(""); const [blockedMessage, setBlockedMessage] = useState(null); @@ -61,7 +65,9 @@ export function DownloadRecoveryKeyPage({ hasCredentialPassword }: Props) { }; const handleSkip = () => { - skipRecoveryKeyDownload(); + if (!userId) return; + + document.cookie = `${RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME}=${userId}; path=/; max-age=${RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_MAX_AGE}`; void navigate({ to: "/volumes", replace: true }); }; diff --git a/app/client/modules/auth/routes/login.tsx b/app/client/modules/auth/routes/login.tsx index 76340543..ec908561 100644 --- a/app/client/modules/auth/routes/login.tsx +++ b/app/client/modules/auth/routes/login.tsx @@ -10,7 +10,7 @@ import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "~/clie import { Label } from "~/client/components/ui/label"; import { authClient } from "~/client/lib/auth-client"; import { logger } from "~/client/lib/logger"; -import { hasSkippedRecoveryKeyDownload } from "~/client/lib/recovery-key-skip"; +import { RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME } from "~/lib/recovery-key-skip"; import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/sso-errors"; import { ResetPasswordDialog } from "../components/reset-password-dialog"; import { useNavigate } from "@tanstack/react-router"; @@ -33,6 +33,12 @@ type LoginPageProps = { error?: string; }; +function hasSkippedRecoveryKeyDownload(userId: string) { + return document.cookie + .split(";") + .some((cookie) => cookie.trim() === `${RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME}=${userId}`); +} + export function LoginPage({ error }: LoginPageProps = {}) { const navigate = useNavigate(); const [showResetDialog, setShowResetDialog] = useState(false); @@ -78,7 +84,7 @@ export function LoginPage({ error }: LoginPageProps = {}) { } const d = await authClient.getSession(); - if (data.user && !d.data?.user.hasDownloadedResticPassword && !hasSkippedRecoveryKeyDownload()) { + if (data.user && !d.data?.user.hasDownloadedResticPassword && !hasSkippedRecoveryKeyDownload(data.user.id)) { void navigate({ to: "/download-recovery-key" }); } else { void navigate({ to: "/volumes" }); @@ -117,7 +123,7 @@ export function LoginPage({ error }: LoginPageProps = {}) { if ( session.data?.user && !session.data.user.hasDownloadedResticPassword && - !hasSkippedRecoveryKeyDownload() + !hasSkippedRecoveryKeyDownload(session.data.user.id) ) { void navigate({ to: "/download-recovery-key" }); } else { diff --git a/app/context.ts b/app/context.ts index 083461eb..a8a32d16 100644 --- a/app/context.ts +++ b/app/context.ts @@ -15,4 +15,5 @@ export type AppContext = { user: User | null; hasUsers: boolean; sidebarOpen: boolean; + hasSkippedRecoveryKeyDownload: boolean; }; diff --git a/app/lib/recovery-key-skip.ts b/app/lib/recovery-key-skip.ts new file mode 100644 index 00000000..48df6005 --- /dev/null +++ b/app/lib/recovery-key-skip.ts @@ -0,0 +1,2 @@ +export const RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME = "zerobyte_recovery_key_download_skipped"; +export const RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_MAX_AGE = 60 * 60; diff --git a/app/middleware/auth.ts b/app/middleware/auth.ts index 697c4381..3de945c8 100644 --- a/app/middleware/auth.ts +++ b/app/middleware/auth.ts @@ -1,9 +1,10 @@ import { createMiddleware } from "@tanstack/react-start"; import { redirect } from "@tanstack/react-router"; import { auth } from "~/server/lib/auth"; -import { getRequestHeaders } from "@tanstack/react-start/server"; +import { getCookie, getRequestHeaders } from "@tanstack/react-start/server"; import { authService } from "~/server/modules/auth/auth.service"; import { isAuthRoute } from "~/lib/auth-routes"; +import { RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME } from "~/lib/recovery-key-skip"; export const authMiddleware = createMiddleware().server(async ({ next, request }) => { const headers = getRequestHeaders(); @@ -20,7 +21,13 @@ export const authMiddleware = createMiddleware().server(async ({ next, request } } if (session?.user?.id) { - if (!session.user.hasDownloadedResticPassword && pathname !== "/download-recovery-key") { + const hasSkippedRecoveryKeyDownload = getCookie(RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME) === session.user.id; + + if ( + !session.user.hasDownloadedResticPassword && + !hasSkippedRecoveryKeyDownload && + pathname !== "/download-recovery-key" + ) { throw redirect({ to: "/download-recovery-key" }); } diff --git a/app/routes/(auth)/download-recovery-key.tsx b/app/routes/(auth)/download-recovery-key.tsx index 27b28137..385c9986 100644 --- a/app/routes/(auth)/download-recovery-key.tsx +++ b/app/routes/(auth)/download-recovery-key.tsx @@ -11,6 +11,7 @@ const getRecoveryKeyUserState = createServerFn({ method: "GET" }).handler(async return { hasCredentialPassword: session?.user ? await userHasCredentialPassword(session.user.id) : false, + userId: session?.user.id ?? null, }; }); @@ -30,7 +31,7 @@ export const Route = createFileRoute("/(auth)/download-recovery-key")({ }); function RouteComponent() { - const { hasCredentialPassword } = Route.useLoaderData(); + const { hasCredentialPassword, userId } = Route.useLoaderData(); - return ; + return ; } diff --git a/app/routes/(dashboard)/route.tsx b/app/routes/(dashboard)/route.tsx index 53814c5f..af2c6649 100644 --- a/app/routes/(dashboard)/route.tsx +++ b/app/routes/(dashboard)/route.tsx @@ -9,7 +9,7 @@ import { getOrganizationContext } from "~/server/lib/functions/organization-cont import { getServerConstants } from "~/server/lib/functions/server-constants"; import { userHasCredentialPassword } from "~/server/modules/auth/helpers"; import { authService } from "~/server/modules/auth/auth.service"; -import { hasSkippedRecoveryKeyDownload } from "~/client/lib/recovery-key-skip"; +import { RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME } from "~/lib/recovery-key-skip"; export const fetchUser = createServerFn({ method: "GET" }).handler(async () => { const headers = getRequestHeaders(); @@ -19,11 +19,14 @@ export const fetchUser = createServerFn({ method: "GET" }).handler(async () => { const sidebarCookie = getCookie(SIDEBAR_COOKIE_NAME); const sidebarOpen = !sidebarCookie ? true : sidebarCookie === "true"; + const hasSkippedRecoveryKeyDownload = + !!session?.user && getCookie(RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME) === session.user.id; return { user: session?.user ? { ...session.user, hasCredentialPassword } : null, hasUsers, sidebarOpen, + hasSkippedRecoveryKeyDownload, }; }); @@ -46,7 +49,11 @@ export const Route = createFileRoute("/(dashboard)")({ }), ]); - if (authContext.user && !authContext.user.hasDownloadedResticPassword && !hasSkippedRecoveryKeyDownload()) { + if ( + authContext.user && + !authContext.user.hasDownloadedResticPassword && + !authContext.hasSkippedRecoveryKeyDownload + ) { throw redirect({ to: "/download-recovery-key" }); }