diff --git a/app/client/modules/auth/routes/download-recovery-key.tsx b/app/client/modules/auth/routes/download-recovery-key.tsx index 2ff2b6ae..842f3307 100644 --- a/app/client/modules/auth/routes/download-recovery-key.tsx +++ b/app/client/modules/auth/routes/download-recovery-key.tsx @@ -9,16 +9,21 @@ 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 { + 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 = - "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; + 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); @@ -56,11 +61,14 @@ export function DownloadRecoveryKeyPage({ hasCredentialPassword }: Props) { } setBlockedMessage(null); - downloadResticPassword.mutate({ - body: { - password, - }, - }); + downloadResticPassword.mutate({ body: { password } }); + }; + + const handleSkip = () => { + 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 }); }; return ( @@ -114,6 +122,15 @@ export function DownloadRecoveryKeyPage({ hasCredentialPassword }: Props) { Download Recovery Key )} + diff --git a/app/client/modules/auth/routes/login.tsx b/app/client/modules/auth/routes/login.tsx index 9e9aa9f3..ec908561 100644 --- a/app/client/modules/auth/routes/login.tsx +++ b/app/client/modules/auth/routes/login.tsx @@ -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 { 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"; @@ -32,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); @@ -77,7 +84,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(data.user.id)) { void navigate({ to: "/download-recovery-key" }); } else { void navigate({ to: "/volumes" }); @@ -113,7 +120,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(session.data.user.id) + ) { void navigate({ to: "/download-recovery-key" }); } else { void navigate({ to: "/volumes" }); @@ -130,7 +141,10 @@ export function LoginPage({ error }: LoginPageProps = {}) { if (requires2FA) { return ( - +
@@ -199,7 +213,11 @@ export function LoginPage({ error }: LoginPageProps = {}) {
-
+
{errorDescription}
{ 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 5e88ece6..af2c6649 100644 --- a/app/routes/(dashboard)/route.tsx +++ b/app/routes/(dashboard)/route.tsx @@ -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 { RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME } from "~/lib/recovery-key-skip"; export const fetchUser = createServerFn({ method: "GET" }).handler(async () => { const headers = getRequestHeaders(); @@ -18,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, }; }); @@ -45,7 +49,11 @@ export const Route = createFileRoute("/(dashboard)")({ }), ]); - if (authContext.user && !authContext.user.hasDownloadedResticPassword) { + if ( + authContext.user && + !authContext.user.hasDownloadedResticPassword && + !authContext.hasSkippedRecoveryKeyDownload + ) { throw redirect({ to: "/download-recovery-key" }); }