add bearer token modal when auth is missing
All checks were successful
Build & Deploy / deploy (push) Successful in 1m41s
All checks were successful
Build & Deploy / deploy (push) Successful in 1m41s
Modal automatically opens on mount if no token is stored, and reopens on any 401 from the API via a global "auth-required" event. Token is persisted in localStorage under "ordinarthur.bearer". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8ef642fb0b
commit
810575221f
@ -41,6 +41,9 @@ export async function api<T = unknown>(
|
||||
}
|
||||
const res = await fetch(`${BASE}${path}`, { ...rest, headers: finalHeaders });
|
||||
if (!res.ok) {
|
||||
if (res.status === 401 && auth) {
|
||||
window.dispatchEvent(new CustomEvent("auth-required"));
|
||||
}
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new ApiError(res.status, text || res.statusText);
|
||||
}
|
||||
@ -66,6 +69,9 @@ export async function apiBinary<T = unknown>(
|
||||
body,
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
window.dispatchEvent(new CustomEvent("auth-required"));
|
||||
}
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new ApiError(res.status, text || res.statusText);
|
||||
}
|
||||
|
||||
118
apps/pwa/src/components/shell/BearerTokenModal.tsx
Normal file
118
apps/pwa/src/components/shell/BearerTokenModal.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { getToken, setToken, clearToken } from "@/api/client";
|
||||
import { Label } from "@/design";
|
||||
|
||||
/**
|
||||
* Modal globale pour saisir / mettre à jour le bearer token.
|
||||
* S'affiche automatiquement :
|
||||
* - au mount si aucun token en localStorage
|
||||
* - quand `client.ts` dispatche l'event "auth-required" (ex: 401)
|
||||
*/
|
||||
export function BearerTokenModal() {
|
||||
const [open, setOpen] = useState(() => !getToken());
|
||||
const [value, setValue] = useState("");
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function onAuthRequired() {
|
||||
setValue("");
|
||||
setOpen(true);
|
||||
}
|
||||
window.addEventListener("auth-required", onAuthRequired);
|
||||
return () => window.removeEventListener("auth-required", onAuthRequired);
|
||||
}, []);
|
||||
|
||||
function handleSave(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return;
|
||||
setToken(trimmed);
|
||||
setOpen(false);
|
||||
// Recharger pour que toutes les queries React repartent avec le token
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
clearToken();
|
||||
setValue("");
|
||||
}
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[60] bg-ink/60 flex items-end sm:items-center justify-center p-0 sm:p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
className="w-full sm:max-w-md bg-bg border-t-2 sm:border-2 border-ink flex flex-col"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<header className="border-b border-ink px-4 py-3">
|
||||
<Label prefix="[ AUTH ]">BEARER TOKEN REQUIS</Label>
|
||||
</header>
|
||||
|
||||
<form onSubmit={handleSave} className="px-4 py-4 space-y-4">
|
||||
<p className="font-sans text-sm text-ink">
|
||||
Colle ton token pour accéder à l'API.
|
||||
Il sera stocké en <em className="text-accent not-italic">localStorage</em> (clé{" "}
|
||||
<code className="font-mono text-xs">ordinarthur.bearer</code>).
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<Label className="mb-1 block">TOKEN</Label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type={show ? "text" : "password"}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
placeholder="xxxxxxxxxxxxxxxxxxxx"
|
||||
className="flex-1 border border-ink bg-bg px-3 py-2 font-mono text-sm text-ink focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShow((s) => !s)}
|
||||
className="border border-ink px-3 py-2 font-mono text-[10px] uppercase tracking-label text-muted hover:text-ink"
|
||||
>
|
||||
{show ? "Cacher" : "Voir"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="flex items-center justify-between gap-3 pt-2 border-t border-ink -mx-4 px-4 -mb-4 pb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="font-mono text-[10px] uppercase tracking-label text-muted hover:text-accent"
|
||||
>
|
||||
Effacer
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{getToken() && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
className="border border-ink px-4 py-2 font-mono text-[11px] uppercase tracking-label text-ink hover:bg-ink hover:text-bg"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!value.trim()}
|
||||
className="border border-ink bg-ink px-4 py-2 font-mono text-[11px] uppercase tracking-label text-bg hover:bg-accent hover:border-accent disabled:opacity-40"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { Outlet, createRootRoute, Link } from "@tanstack/react-router";
|
||||
import { AccentDot, Label } from "@/design";
|
||||
import { BottomNav } from "@/components/shell/BottomNav";
|
||||
import { BearerTokenModal } from "@/components/shell/BearerTokenModal";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootLayout,
|
||||
@ -40,6 +41,7 @@ function RootLayout() {
|
||||
</footer>
|
||||
|
||||
<BottomNav />
|
||||
<BearerTokenModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user