From 810575221f8a92b587d104a3b87527ba956240aa Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Thu, 16 Apr 2026 17:29:55 +0200 Subject: [PATCH] add bearer token modal when auth is missing 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 --- apps/pwa/src/api/client.ts | 6 + .../src/components/shell/BearerTokenModal.tsx | 118 ++++++++++++++++++ apps/pwa/src/routes/__root.tsx | 2 + 3 files changed, 126 insertions(+) create mode 100644 apps/pwa/src/components/shell/BearerTokenModal.tsx diff --git a/apps/pwa/src/api/client.ts b/apps/pwa/src/api/client.ts index d9e7177..6486344 100644 --- a/apps/pwa/src/api/client.ts +++ b/apps/pwa/src/api/client.ts @@ -41,6 +41,9 @@ export async function api( } 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( 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); } diff --git a/apps/pwa/src/components/shell/BearerTokenModal.tsx b/apps/pwa/src/components/shell/BearerTokenModal.tsx new file mode 100644 index 0000000..80b94ca --- /dev/null +++ b/apps/pwa/src/components/shell/BearerTokenModal.tsx @@ -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 ( +
+
e.stopPropagation()} + > +
+ +
+ +
+

+ Colle ton token pour accéder à l'API. + Il sera stocké en localStorage (clé{" "} + ordinarthur.bearer). +

+ +
+ +
+ 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" + /> + +
+
+ +
+ + +
+ {getToken() && ( + + )} + +
+
+
+
+
+ ); +} diff --git a/apps/pwa/src/routes/__root.tsx b/apps/pwa/src/routes/__root.tsx index e0c85db..68030c9 100644 --- a/apps/pwa/src/routes/__root.tsx +++ b/apps/pwa/src/routes/__root.tsx @@ -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() { + ); }