This commit is contained in:
timka123 committed 2026-10-19 09:41:32 +03:00
1 parent 4bdbc8bfa8
commit 2be074f191
22 files changed
+1034 -25

No files matched your search

+13 -1
View File
@@ -26,6 +26,15 @@ export function setToken(token: string): void {
else localStorage.removeItem(TOKEN_KEY);
}
// The login screen listens here, so a session that expires mid-use drops the
// app back to it instead of showing a generic "backend unreachable" error.
const unauthorizedListeners = new Set<() => void>();
export function onUnauthorized(listener: () => void): () => void {
unauthorizedListeners.add(listener);
return () => unauthorizedListeners.delete(listener);
}
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const headers = new Headers(init.headers);
headers.set("Accept", "application/json");
@@ -33,7 +42,10 @@ async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const token = getToken();
if (token) headers.set("X-API-Token", token);
const response = await fetch(path, { ...init, headers });
const response = await fetch(path, { ...init, headers, credentials: "same-origin" });
if (response.status === 401 && !path.startsWith("/api/auth/")) {
unauthorizedListeners.forEach((listener) => listener());
}
if (!response.ok) {
let detail = `${response.status} ${response.statusText}`;
try {
+81
View File
@@ -0,0 +1,81 @@
/**
* The login gate.
*
* One fetch at start-up decides whether the app behind it is reachable yet.
* When no password or Telegram login is configured, `authenticated` comes
* back `true` immediately — the same friction-free default the API token has
* always had — so nothing changes until Settings → Access is actually set up.
*/
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
import { api, onUnauthorized } from "./api";
import type { AuthMe } from "./types";
interface AuthContextValue {
me: AuthMe | null;
loading: boolean;
error: string | null;
loginWithPassword: (username: string, password: string) => Promise<void>;
loginWithTelegram: (payload: Record<string, unknown>) => Promise<void>;
logout: () => Promise<void>;
refresh: () => Promise<void>;
}
const Context = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [me, setMe] = useState<AuthMe | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async () => {
try {
const body = await api.get<AuthMe>("/api/auth/me");
setMe(body);
setError(null);
} catch (exc) {
setError(exc instanceof Error ? exc.message : String(exc));
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void refresh();
}, [refresh]);
useEffect(() => onUnauthorized(() => void refresh()), [refresh]);
const loginWithPassword = useCallback(
async (username: string, password: string) => {
await api.post("/api/auth/login", { username, password });
await refresh();
},
[refresh],
);
const loginWithTelegram = useCallback(
async (payload: Record<string, unknown>) => {
await api.post("/api/auth/login/telegram", payload);
await refresh();
},
[refresh],
);
const logout = useCallback(async () => {
await api.post("/api/auth/logout");
await refresh();
}, [refresh]);
const value = useMemo(
() => ({ me, loading, error, loginWithPassword, loginWithTelegram, logout, refresh }),
[me, loading, error, loginWithPassword, loginWithTelegram, logout, refresh],
);
return <Context.Provider value={value}>{children}</Context.Provider>;
}
export function useAuth(): AuthContextValue {
const value = useContext(Context);
if (!value) throw new Error("useAuth outside AuthProvider");
return value;
}
+17
View File
@@ -236,9 +236,26 @@ export interface AppSettings {
};
};
appearance: { theme: "light" | "dark" | "system"; accent: string; compact: boolean };
auth: {
username: string;
password_hash: string;
telegram_login_enabled: boolean;
telegram_owner_id: string;
telegram_bot_username: string;
session_days: number;
};
}
export interface SettingsResponse {
settings: AppSettings;
defaults: { blocked_domains: string[]; blocked_apps: string[] };
}
export interface AuthMe {
authenticated: boolean;
login_required: boolean;
username: string;
password_login_enabled: boolean;
telegram_login_enabled: boolean;
telegram_bot_username: string;
}