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:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user