feat(frontend): polish web (logout, 401→pairing, reconnexion globale) (#13)
Flow logout : POST /api/logout puis retour à l'écran de pairing et déconnexion du live. 401 renvoie au pairing sans boucle. Bannière de reconnexion couvrant le cas live-only et le serveur indisponible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -5,14 +5,15 @@
|
||||
* Routes on the {@link WebSession} paired flag: not paired ⇒ {@link PairingScreen};
|
||||
* paired ⇒ the read-only {@link WebWorkspace}. It subscribes to the session's
|
||||
* `onUnauthorized` signal so a `401` from any `/api/invoke` (expired/missing
|
||||
* cookie) drops back to pairing automatically. A best-effort "Se déconnecter"
|
||||
* clears the local flag (server-side cookie revocation is B8).
|
||||
* cookie) drops back to pairing automatically. "Se déconnecter" (F6) revokes the
|
||||
* server session (`POST /api/logout`), tears down the live WS singleton, and
|
||||
* returns to pairing.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/shared";
|
||||
import { getWebSession, type WebSession } from "@/adapters/http";
|
||||
import { disconnectWebLive, getWebSession, type WebSession } from "@/adapters/http";
|
||||
import { PairingScreen } from "./PairingScreen";
|
||||
import { WebWorkspace } from "./WebWorkspace";
|
||||
|
||||
@ -24,15 +25,26 @@ interface WebAppProps {
|
||||
export function WebApp({ session }: WebAppProps = {}) {
|
||||
const webSession = session ?? getWebSession();
|
||||
const [paired, setPaired] = useState(() => webSession.isPaired());
|
||||
const [signingOut, setSigningOut] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// A 401 anywhere clears the flag and fires this: return to pairing.
|
||||
// A 401 anywhere clears the flag and fires this: return to pairing. The live
|
||||
// WS is torn down by the composition-root 401 handler (F6).
|
||||
return webSession.onUnauthorized(() => setPaired(false));
|
||||
}, [webSession]);
|
||||
|
||||
function signOut(): void {
|
||||
webSession.forget();
|
||||
setPaired(false);
|
||||
async function signOut(): Promise<void> {
|
||||
if (signingOut) return;
|
||||
setSigningOut(true);
|
||||
try {
|
||||
// Revoke the server session (best-effort), then drop the live socket so it
|
||||
// stops reconnecting, and return to pairing regardless of the outcome.
|
||||
await webSession.logout();
|
||||
} finally {
|
||||
disconnectWebLive();
|
||||
setSigningOut(false);
|
||||
setPaired(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@ -45,7 +57,7 @@ export function WebApp({ session }: WebAppProps = {}) {
|
||||
</span>
|
||||
</div>
|
||||
{paired && (
|
||||
<Button variant="ghost" size="sm" onClick={signOut}>
|
||||
<Button variant="ghost" size="sm" loading={signingOut} onClick={() => void signOut()}>
|
||||
Se déconnecter
|
||||
</Button>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user