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 { downloadResticPasswordMutation } from "~/client/api-client/@tanstack/react-query.gen";
import { parseError } from "~/client/lib/errors";
import { skipRecoveryKeyDownload } from "~/client/lib/recovery-key-skip";
import { useNavigate } from "@tanstack/react-router";
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 = {
hasCredentialPassword: boolean;
@ -56,11 +57,12 @@ export function DownloadRecoveryKeyPage({ hasCredentialPassword }: Props) {
}
setBlockedMessage(null);
downloadResticPassword.mutate({
body: {
password,
},
});
downloadResticPassword.mutate({ body: { password } });
};
const handleSkip = () => {
skipRecoveryKeyDownload();
void navigate({ to: "/volumes", replace: true });
};
return (
@ -114,6 +116,15 @@ export function DownloadRecoveryKeyPage({ hasCredentialPassword }: Props) {
Download Recovery Key
</Button>
)}
<Button
type="button"
variant="ghost"
onClick={handleSkip}
disabled={downloadResticPassword.isPending}
className="w-full"
>
Skip
</Button>
</div>
</form>
</AuthLayout>

View file

@ -10,6 +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 { decodeLoginError, getLoginErrorDescription } from "~/client/lib/sso-errors";
import { ResetPasswordDialog } from "../components/reset-password-dialog";
import { useNavigate } from "@tanstack/react-router";
@ -77,7 +78,7 @@ export function LoginPage({ error }: LoginPageProps = {}) {
}
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" });
} else {
void navigate({ to: "/volumes" });
@ -113,7 +114,11 @@ export function LoginPage({ error }: LoginPageProps = {}) {
if (data) {
toast.success("Login successful");
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" });
} else {
void navigate({ to: "/volumes" });
@ -130,7 +135,10 @@ export function LoginPage({ error }: LoginPageProps = {}) {
if (requires2FA) {
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-4 flex flex-col items-center">
<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">
<Form {...form}>
<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}
</div>
<FormField

View file

@ -49,7 +49,7 @@ import { useOrganizationContext } from "~/client/hooks/use-org-context";
import { cn } from "~/client/lib/utils";
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 = {
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 { userHasCredentialPassword } from "~/server/modules/auth/helpers";
import { authService } from "~/server/modules/auth/auth.service";
import { hasSkippedRecoveryKeyDownload } from "~/client/lib/recovery-key-skip";
export const fetchUser = createServerFn({ method: "GET" }).handler(async () => {
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" });
}