LogoutButton.jsx 1.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. // ---------------------------------------------------------------------------
  2. // Folder: components/auth
  3. // File: LogoutButton.jsx
  4. // Relative Path: components/auth/LogoutButton.jsx
  5. // ---------------------------------------------------------------------------
  6. "use client";
  7. import React from "react";
  8. import { logout } from "@/lib/frontend/apiClient";
  9. import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
  10. import { Button } from "@/components/ui/button";
  11. /**
  12. * LogoutButton (RHL-020)
  13. *
  14. * Responsibilities:
  15. * - Call apiClient.logout() to clear the HTTP-only session cookie.
  16. * - Then redirect to /login?reason=logged-out.
  17. *
  18. * Important test/runtime note:
  19. * - We intentionally avoid next/navigation hooks here.
  20. * - Some unit tests render AppShell via react-dom/server without Next.js runtime.
  21. * - Using window.location inside the click handler avoids needing router context
  22. * during server rendering (handler is not invoked in SSR tests).
  23. */
  24. export default function LogoutButton() {
  25. const [isLoggingOut, setIsLoggingOut] = React.useState(false);
  26. async function handleLogout() {
  27. if (isLoggingOut) return;
  28. setIsLoggingOut(true);
  29. try {
  30. // Backend endpoint is idempotent; even if no cookie exists it returns ok.
  31. await logout();
  32. } catch (err) {
  33. // If logout fails due to network issues, we still redirect to login.
  34. // This keeps UX predictable; user can log in again if needed.
  35. console.error("[LogoutButton] logout failed:", err);
  36. }
  37. const loginUrl = buildLoginUrl({ reason: LOGIN_REASONS.LOGGED_OUT });
  38. // Replace so "Back" won't bring the user into a protected page.
  39. window.location.replace(loginUrl);
  40. }
  41. return (
  42. <Button
  43. variant="outline"
  44. size="sm"
  45. type="button"
  46. disabled={isLoggingOut}
  47. aria-disabled={isLoggingOut ? "true" : "false"}
  48. onClick={handleLogout}
  49. title={isLoggingOut ? "Logging out..." : "Logout"}
  50. >
  51. {isLoggingOut ? "Logging out..." : "Logout"}
  52. </Button>
  53. );
  54. }