fix: security fixes — 401 handler with token refresh, action error feedback in Keys

- client.ts: handle 401 responses by attempting token refresh, then logout and redirect to /login on failure
- Keys.tsx: add actionError state, surface API errors from handleRename and handleFreeze with visible error banner

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 12:36:26 +03:00
parent 42c9954585
commit f3c80de219
2 changed files with 40 additions and 3 deletions
+28
View File
@@ -18,6 +18,34 @@ async function request(
}
const res = await fetch(`${BASE}${url}`, { ...options, headers });
if (res.status === 401 && withAuth) {
const refreshToken = useAuthStore.getState().refreshToken;
if (refreshToken) {
const refreshRes = await fetch(`${BASE}/web/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (refreshRes.ok) {
const data = await refreshRes.json();
if (data.ok) {
useAuthStore.getState().setAuth(
data.access_token,
data.refresh_token,
useAuthStore.getState().user!
);
headers["Authorization"] = `Bearer ${data.access_token}`;
const retryRes = await fetch(`${BASE}${url}`, { ...options, headers });
return retryRes.json();
}
}
}
useAuthStore.getState().logout();
window.location.href = "/login";
return { ok: false, error: "Session expired" };
}
return res.json();
}
+12 -3
View File
@@ -11,19 +11,23 @@ export default function Keys() {
const [qrKey, setQrKey] = useState<any>(null);
const [renaming, setRenaming] = useState<string | null>(null);
const [newName, setNewName] = useState("");
const [actionError, setActionError] = useState<string | null>(null);
const load = () => api.action("profile").then((r) => { if (r.ok) setProfile(r); });
useEffect(() => { load(); }, []);
async function handleRename(clientId: string) {
if (!newName.trim()) return;
await api.action("rename_key", { client_id: clientId, name: newName.trim() });
setRenaming(null); setNewName("");
const res = await api.action("rename_key", { client_id: clientId, name: newName.trim() });
if (!res.ok) { setActionError(res.error || "Ошибка переименования"); return; }
setRenaming(null); setNewName(""); setActionError(null);
load();
}
async function handleFreeze(clientId: string, frozen: boolean) {
await api.action("freeze_key", { client_id: clientId, frozen });
const res = await api.action("freeze_key", { client_id: clientId, frozen });
if (!res.ok) { setActionError(res.error || "Ошибка"); return; }
setActionError(null);
load();
}
@@ -33,6 +37,11 @@ export default function Keys() {
<DashboardLayout>
{qrKey && <QRModal text={qrKey.remnawave_link || qrKey.link} title={qrKey.name} onClose={() => setQrKey(null)} />}
<h1 className="text-2xl font-bold mb-6">Мои ключи</h1>
{actionError && (
<div className="mb-4 px-4 py-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
{actionError}
</div>
)}
{!keys.length && <div className="text-white/40 text-center py-12">Ключей нет. Купите подписку.</div>}
{keys.map((k: any) => (
<GlassCard key={k.client_id} className="mb-4">