feat: branded login screen in front of Basic Auth

The browser's native Basic Auth prompt can't be restyled — it's chrome UI,
not part of the page. Replace it with Aurum's own login screen: nginx keeps
auth_basic on /api/* (the actual protection, unchanged), but drops it from
the SPA shell (just HTML/JS/CSS, no data) so the app can render its own
branded form instead. On submit the app validates the credentials against a
real protected endpoint, then attaches the resulting Authorization header to
every request itself — sessionStorage-backed, so closing the tab signs out.
Instances with Basic Auth left off (the default) never see this screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
This commit is contained in:
ZProger
2026-08-30 03:05:01 +03:00
parent 76c5caa667
commit 1de9ac272e
8 changed files with 293 additions and 5 deletions

View File

@@ -1,8 +1,15 @@
import { api } from "@/api/client";
import { getAuthHeader } from "@/lib/auth";
import { t } from "@/lib/i18n";
export async function exportBackup(): Promise<void> {
const response = await fetch("/api/backup/export");
// Raw fetch (not api/client.ts's request()) — the response is a file
// download, not JSON — so the Authorization header has to be attached
// here by hand too, same as every other request.
const authHeader = getAuthHeader();
const response = await fetch("/api/backup/export", {
headers: authHeader ? { Authorization: authHeader } : {},
});
if (!response.ok) {
throw new Error(await response.text());
}

View File

@@ -1,3 +1,5 @@
import { clearCredentials, getAuthHeader } from "@/lib/auth";
const API_BASE = "/api";
export class ApiError extends Error {
@@ -9,11 +11,22 @@ export class ApiError extends Error {
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const authHeader = getAuthHeader();
const response = await fetch(`${API_BASE}${path}`, {
headers: { "Content-Type": "application/json" },
headers: {
"Content-Type": "application/json",
...(authHeader ? { Authorization: authHeader } : {}),
},
...init,
});
if (response.status === 401) {
// Stored credentials are missing/stale (password changed, or Basic Auth
// was just turned on) — drop them so LoginGate falls back to the login
// screen instead of every request failing silently forever.
clearCredentials();
}
if (!response.ok) {
const body = await response.text();
let message = body || response.statusText;