feat(auth): allow skipping forced recovery key download

This commit is contained in:
Nicolas Meienberger 2026-05-19 19:28:32 +02:00
parent c071596151
commit 37f19a1a63
No known key found for this signature in database
5 changed files with 47 additions and 12 deletions

View file

@ -0,0 +1,11 @@
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");
}

View file

@ -9,10 +9,11 @@ import { Input } from "~/client/components/ui/input";
import { Label } from "~/client/components/ui/label"; import { Label } from "~/client/components/ui/label";
import { downloadResticPasswordMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { downloadResticPasswordMutation } from "~/client/api-client/@tanstack/react-query.gen";
import { parseError } from "~/client/lib/errors"; import { parseError } from "~/client/lib/errors";
import { skipRecoveryKeyDownload } from "~/client/lib/recovery-key-skip";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
const RECOVERY_KEY_CREDENTIAL_REQUIRED_MESSAGE = const RECOVERY_KEY_CREDENTIAL_REQUIRED_MESSAGE =
"Downloading the recovery key requires a local credential password. Ask an operator to run `bun run cli reset-password` for your user, then sign in with that password and try again."; "Downloading the recovery key requires a local credential password. Ask an operator to run `docker exec -it zerobyte bun run cli reset-password` for your user, then sign in with that password and try again.";
type Props = { type Props = {
hasCredentialPassword: boolean; hasCredentialPassword: boolean;
@ -56,11 +57,12 @@ export function DownloadRecoveryKeyPage({ hasCredentialPassword }: Props) {
} }
setBlockedMessage(null); setBlockedMessage(null);
downloadResticPassword.mutate({ downloadResticPassword.mutate({ body: { password } });
body: { };
password,
}, const handleSkip = () => {
}); skipRecoveryKeyDownload();
void navigate({ to: "/volumes", replace: true });
}; };
return ( return (
@ -114,6 +116,15 @@ export function DownloadRecoveryKeyPage({ hasCredentialPassword }: Props) {
Download Recovery Key Download Recovery Key
</Button> </Button>
)} )}
<Button
type="button"
variant="ghost"
onClick={handleSkip}
disabled={downloadResticPassword.isPending}
className="w-full"
>
Skip
</Button>
</div> </div>
</form> </form>
</AuthLayout> </AuthLayout>

View file

@ -10,6 +10,7 @@ import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "~/clie
import { Label } from "~/client/components/ui/label"; import { Label } from "~/client/components/ui/label";
import { authClient } from "~/client/lib/auth-client"; import { authClient } from "~/client/lib/auth-client";
import { logger } from "~/client/lib/logger"; import { logger } from "~/client/lib/logger";
import { hasSkippedRecoveryKeyDownload } from "~/client/lib/recovery-key-skip";
import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/sso-errors"; import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/sso-errors";
import { ResetPasswordDialog } from "../components/reset-password-dialog"; import { ResetPasswordDialog } from "../components/reset-password-dialog";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
@ -77,7 +78,7 @@ export function LoginPage({ error }: LoginPageProps = {}) {
} }
const d = await authClient.getSession(); const d = await authClient.getSession();
if (data.user && !d.data?.user.hasDownloadedResticPassword) { if (data.user && !d.data?.user.hasDownloadedResticPassword && !hasSkippedRecoveryKeyDownload()) {
void navigate({ to: "/download-recovery-key" }); void navigate({ to: "/download-recovery-key" });
} else { } else {
void navigate({ to: "/volumes" }); void navigate({ to: "/volumes" });
@ -113,7 +114,11 @@ export function LoginPage({ error }: LoginPageProps = {}) {
if (data) { if (data) {
toast.success("Login successful"); toast.success("Login successful");
const session = await authClient.getSession(); const session = await authClient.getSession();
if (session.data?.user && !session.data.user.hasDownloadedResticPassword) { if (
session.data?.user &&
!session.data.user.hasDownloadedResticPassword &&
!hasSkippedRecoveryKeyDownload()
) {
void navigate({ to: "/download-recovery-key" }); void navigate({ to: "/download-recovery-key" });
} else { } else {
void navigate({ to: "/volumes" }); void navigate({ to: "/volumes" });
@ -130,7 +135,10 @@ export function LoginPage({ error }: LoginPageProps = {}) {
if (requires2FA) { if (requires2FA) {
return ( return (
<AuthLayout title="Two-Factor Authentication" description="Enter the 6-digit code from your authenticator app"> <AuthLayout
title="Two-Factor Authentication"
description="Enter the 6-digit code from your authenticator app"
>
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-4 flex flex-col items-center"> <div className="space-y-4 flex flex-col items-center">
<Label htmlFor="totp-code">Authentication code</Label> <Label htmlFor="totp-code">Authentication code</Label>
@ -199,7 +207,11 @@ export function LoginPage({ error }: LoginPageProps = {}) {
<AuthLayout title="Login to your account" description="Enter your credentials below to login to your account"> <AuthLayout title="Login to your account" description="Enter your credentials below to login to your account">
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className={cn("rounded-md border border-destructive/50 p-3 text-sm", { hidden: !errorDescription })}> <div
className={cn("rounded-md border border-destructive/50 p-3 text-sm", {
hidden: !errorDescription,
})}
>
{errorDescription} {errorDescription}
</div> </div>
<FormField <FormField

View file

@ -49,7 +49,7 @@ import { useOrganizationContext } from "~/client/hooks/use-org-context";
import { cn } from "~/client/lib/utils"; import { cn } from "~/client/lib/utils";
const RECOVERY_KEY_CREDENTIAL_REQUIRED_MESSAGE = const RECOVERY_KEY_CREDENTIAL_REQUIRED_MESSAGE =
"Downloading the recovery key requires a local credential password. Ask an operator to run `bun run cli reset-password` for your user, then sign in with that password and try again."; "Downloading the recovery key requires a local credential password. Ask an operator to run `docker exec -it zerobyte bun run cli reset-password` for your user, then sign in with that password and try again.";
type Props = { type Props = {
appContext: AppContext; appContext: AppContext;

View file

@ -9,6 +9,7 @@ import { getOrganizationContext } from "~/server/lib/functions/organization-cont
import { getServerConstants } from "~/server/lib/functions/server-constants"; import { getServerConstants } from "~/server/lib/functions/server-constants";
import { userHasCredentialPassword } from "~/server/modules/auth/helpers"; import { userHasCredentialPassword } from "~/server/modules/auth/helpers";
import { authService } from "~/server/modules/auth/auth.service"; import { authService } from "~/server/modules/auth/auth.service";
import { hasSkippedRecoveryKeyDownload } from "~/client/lib/recovery-key-skip";
export const fetchUser = createServerFn({ method: "GET" }).handler(async () => { export const fetchUser = createServerFn({ method: "GET" }).handler(async () => {
const headers = getRequestHeaders(); const headers = getRequestHeaders();
@ -45,7 +46,7 @@ export const Route = createFileRoute("/(dashboard)")({
}), }),
]); ]);
if (authContext.user && !authContext.user.hasDownloadedResticPassword) { if (authContext.user && !authContext.user.hasDownloadedResticPassword && !hasSkippedRecoveryKeyDownload()) {
throw redirect({ to: "/download-recovery-key" }); throw redirect({ to: "/download-recovery-key" });
} }