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",