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 });
|
const res = await fetch(`${BASE}${path}`, { ...rest, headers: finalHeaders });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
if (res.status === 401 && auth) {
|
||||||
|
window.dispatchEvent(new CustomEvent("auth-required"));
|
||||||
|
}
|
||||||
const text = await res.text().catch(() => "");
|
const text = await res.text().catch(() => "");
|
||||||
throw new ApiError(res.status, text || res.statusText);
|
throw new ApiError(res.status, text || res.statusText);
|
||||||
}
|
}
|
||||||
@ -66,6 +69,9 @@ export async function apiBinary<T = unknown>(
|
|||||||
body,
|
body,
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
if (res.status === 401) {
|
||||||
|
window.dispatchEvent(new CustomEvent("auth-required"));
|
||||||
|
}
|
||||||
const text = await res.text().catch(() => "");
|
const text = await res.text().catch(() => "");
|
||||||
throw new ApiError(res.status, text || res.statusText);
|
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 { Outlet, createRootRoute, Link } from "@tanstack/react-router";
|
||||||
import { AccentDot, Label } from "@/design";
|
import { AccentDot, Label } from "@/design";
|
||||||
import { BottomNav } from "@/components/shell/BottomNav";
|
import { BottomNav } from "@/components/shell/BottomNav";
|
||||||
|
import { BearerTokenModal } from "@/components/shell/BearerTokenModal";
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: RootLayout,
|
component: RootLayout,
|
||||||
@ -40,6 +41,7 @@ function RootLayout() {
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<BottomNav />
|
<BottomNav />
|
||||||
|
<BearerTokenModal />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user