// auth.jsx — login-форма + хелперы для API-вызовов. // // Сессия — signed cookie 'zxckotik_admin' (ставит бэк). Фронт не хранит // пароль; для определения статуса делает /api/auth/me. const { useState: useStateAuth, useEffect: useEffectAuth } = React; /** GET /api/auth/me — true если есть валидная сессия. */ async function checkAuth() { try { const res = await fetch("/api/auth/me"); if (!res.ok) return false; const data = await res.json(); return Boolean(data.authenticated); } catch { return false; } } /** POST /api/auth/login — отправить пароль, на успех cookie ставится автоматом. */ async function loginWithPassword(password) { try { const res = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password }), }); return res.ok; } catch { return false; } } async function logoutSession() { try { await fetch("/api/auth/logout", { method: "POST" }); } catch {} } /** Универсальный wrapper для mutating запросов. На 401 → window-event для login. */ async function apiMutate(method, url, body) { const opts = { method, headers: body ? { "Content-Type": "application/json" } : {}, body: body ? JSON.stringify(body) : undefined, }; const res = await fetch(url, opts); if (res.status === 401) { window.dispatchEvent(new CustomEvent("auth-required")); throw new Error("auth_required"); } if (!res.ok) { const detail = await res.text().catch(() => ""); throw new Error(`${method} ${url} → ${res.status} ${detail.slice(0, 200)}`); } const ct = res.headers.get("content-type") || ""; return ct.includes("application/json") ? res.json() : null; } function LoginScreen({ onSuccess }) { const [password, setPassword] = useStateAuth(""); const [loading, setLoading] = useStateAuth(false); const [error, setError] = useStateAuth(""); const submit = async (e) => { e.preventDefault(); if (!password) return; setLoading(true); setError(""); const ok = await loginWithPassword(password); setLoading(false); if (ok) { setPassword(""); onSuccess(); } else { setError("Неверный пароль"); } }; return (
Zxckotik
Admin Panel
{error && (
{error}
)}
Пароль задаётся в .env переменной WEB_ADMIN_PASSWORD. При первом запуске сервер сгенерирует случайный и напишет в логи.
); } /** * Универсальная кнопка «Сохранить» с inline-индикатором. * Props: * - onSave: async () => void — выполняет запрос, бросает на ошибке * - label: текст кнопки в idle (по умолчанию «Сохранить») */ function SaveButton({ onSave, label = "Сохранить" }) { const [state, setState] = useStateAuth("idle"); // idle | saving | ok | err const [err, setErr] = useStateAuth(""); const click = async () => { setState("saving"); setErr(""); try { await onSave(); setState("ok"); setTimeout(() => setState("idle"), 1800); } catch (e) { setErr(String(e.message || e).slice(0, 120)); setState("err"); setTimeout(() => setState("idle"), 3500); } }; const cls = { idle: "bg-indigo-500 hover:bg-indigo-600 text-white", saving: "bg-zinc-700 text-zinc-300 cursor-wait", ok: "bg-emerald-500 text-white", err: "bg-rose-500 text-white", }[state]; const text = { idle: label, saving: "Сохраняю…", ok: "✓ Сохранено", err: "✗ Ошибка", }[state]; return (
{state === "err" && err && (
{err}
)}
); } /* expose */ Object.assign(window, { checkAuth, loginWithPassword, logoutSession, apiMutate, LoginScreen, SaveButton, });