diff --git a/frontend/src/components/auth/LoginGate.tsx b/frontend/src/components/auth/LoginGate.tsx index deaee86..ed14796 100644 --- a/frontend/src/components/auth/LoginGate.tsx +++ b/frontend/src/components/auth/LoginGate.tsx @@ -1,13 +1,13 @@ import { type ReactNode, useEffect, useState } from "react"; import { Logo } from "@/components/layout/Logo"; import { LoginScreen } from "@/components/auth/LoginScreen"; -import { NoAuthBanner } from "@/components/auth/NoAuthBanner"; +import { NoAuthModal } from "@/components/auth/NoAuthModal"; import { checkCredentials, useAuthHeader } from "@/lib/auth"; // "not-configured" and "authenticated" both render `children` the same way, // but they must stay distinct: only "not-configured" means this instance has // no AURUM_BASIC_AUTH_USER/PASSWORD at all (nginx never sent 401 to our -// deliberately-wrong probe header), which is what triggers NoAuthBanner. +// deliberately-wrong probe header), which is what triggers NoAuthModal. // Collapsing them back into one "not-required" state would make an // authenticated session with valid stored credentials show the "this // instance has no password" warning too. @@ -56,7 +56,7 @@ export function LoginGate({ children }: { children: ReactNode }) { return ( <> - {probe === "not-configured" && } + {probe === "not-configured" && } {children} ); diff --git a/frontend/src/components/auth/NoAuthBanner.tsx b/frontend/src/components/auth/NoAuthBanner.tsx deleted file mode 100644 index 227cd75..0000000 --- a/frontend/src/components/auth/NoAuthBanner.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useState } from "react"; -import { TriangleAlert, X } from "lucide-react"; -import { useTranslation } from "@/lib/i18n"; - -const DISMISS_KEY = "aurum:noAuthBanner:dismissed"; - -/** Shown by LoginGate for the entire session whenever this instance has no - * AURUM_BASIC_AUTH_USER/PASSWORD configured (see frontend/docker-entrypoint.d/ - * 20-basic-auth.sh) — that state is otherwise only logged to the container's - * stderr at startup, which most self-hosters never look at. Dismissal is - * remembered only in sessionStorage, not localStorage, so the warning comes - * back on the next visit instead of being silence-able forever. */ -export function NoAuthBanner() { - const { t } = useTranslation(); - const [dismissed, setDismissed] = useState(() => { - try { - return sessionStorage.getItem(DISMISS_KEY) === "1"; - } catch { - return false; - } - }); - - if (dismissed) return null; - - function handleDismiss() { - setDismissed(true); - try { - sessionStorage.setItem(DISMISS_KEY, "1"); - } catch { - // storage unavailable — banner just reappears on next render, harmless - } - } - - return ( -
- - {t("auth.noAuthBanner")} - -
- ); -} diff --git a/frontend/src/components/auth/NoAuthModal.tsx b/frontend/src/components/auth/NoAuthModal.tsx new file mode 100644 index 0000000..7a1e30b --- /dev/null +++ b/frontend/src/components/auth/NoAuthModal.tsx @@ -0,0 +1,53 @@ +import { useState } from "react"; +import { ShieldAlert } from "lucide-react"; +import { useTranslation } from "@/lib/i18n"; + +const ACK_KEY = "aurum:noAuthAcknowledged"; + +/** Shown by LoginGate once (ever, per browser) when this instance has no + * AURUM_BASIC_AUTH_USER/PASSWORD configured (see frontend/docker-entrypoint.d/ + * 20-basic-auth.sh) — that state is otherwise only logged to the container's + * stderr at startup, which most self-hosters never look at. Unlike a normal + * Dialog, there is deliberately no backdrop-click, Escape, or X-button way + * out — the only way past it is the explicit "I understand the risk" button, + * whose click is what persists the acknowledgement. */ +export function NoAuthModal() { + const { t } = useTranslation(); + const [acknowledged, setAcknowledged] = useState(() => { + try { + return localStorage.getItem(ACK_KEY) === "1"; + } catch { + return false; + } + }); + + if (acknowledged) return null; + + function handleAccept() { + setAcknowledged(true); + try { + localStorage.setItem(ACK_KEY, "1"); + } catch { + // storage unavailable — modal just reappears next reload, harmless + } + } + + return ( +
+
+
+ +
+

{t("auth.noAuthModalTitle")}

+

{t("auth.noAuthModalBody")}

+ +
+
+ ); +} diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts index 6d3a2f8..cf3d72e 100644 --- a/frontend/src/lib/i18n.ts +++ b/frontend/src/lib/i18n.ts @@ -54,8 +54,9 @@ const ru = { "auth.submitting": "Вход…", "auth.errorInvalidCredentials": "Неверный логин или пароль", "auth.errorUnreachable": "Не удалось подключиться к серверу. Проверьте соединение и попробуйте снова.", - "auth.noAuthBanner": "Этот экземпляр Aurum не защищён паролем — все данные доступны любому, кто до него достучится. Задайте AURUM_BASIC_AUTH_USER и AURUM_BASIC_AUTH_PASSWORD в .env, чтобы включить вход.", - "auth.noAuthBannerDismiss": "Понятно", + "auth.noAuthModalTitle": "Авторизация не настроена", + "auth.noAuthModalBody": "Этот экземпляр Aurum не защищён паролем — все данные доступны любому, кто до него достучится. Задайте AURUM_BASIC_AUTH_USER и AURUM_BASIC_AUTH_PASSWORD в .env, чтобы включить вход.", + "auth.noAuthModalAccept": "Я понимаю риски и хочу продолжить", "dashboard.errorLoading": "Не удалось загрузить данные дашборда. Проверьте, что backend запущен.", "dashboard.statRealIncomeLabel": "Реальный доход", @@ -560,8 +561,9 @@ const en: Record = { "auth.submitting": "Signing in…", "auth.errorInvalidCredentials": "Incorrect username or password", "auth.errorUnreachable": "Couldn't reach the server. Check your connection and try again.", - "auth.noAuthBanner": "This Aurum instance has no password protection — all data is accessible to anyone who can reach it. Set AURUM_BASIC_AUTH_USER and AURUM_BASIC_AUTH_PASSWORD in .env to enable login.", - "auth.noAuthBannerDismiss": "Got it", + "auth.noAuthModalTitle": "Authentication not configured", + "auth.noAuthModalBody": "This Aurum instance has no password protection — all data is accessible to anyone who can reach it. Set AURUM_BASIC_AUTH_USER and AURUM_BASIC_AUTH_PASSWORD in .env to enable login.", + "auth.noAuthModalAccept": "I understand the risk and want to continue", "dashboard.errorLoading": "Failed to load dashboard data. Check that the backend is running.", "dashboard.statRealIncomeLabel": "Real income",