From e2362c6033ed5716ff50066908b6dac9fa061b5d Mon Sep 17 00:00:00 2001 From: nessi Date: Tue, 17 Mar 2026 19:39:13 +0100 Subject: [PATCH] feat: add logout functionality and auto-logout on 401 responses Add AUTH_EXPIRED_EVENT constant and dispatch event on 401 responses in API client, clearing stored token. Add handleLogout function to App component and wire up event listener to trigger logout on auth expiration. Pass onLogout prop to Layout component and add Logout button to topbar-actions. Update CSS to apply flex layout to topbar-actions and make responsive. Add backend hostname and network aliases in docker-compose to ensure consistent --- admin-web/src/api/client.ts | 5 +++++ admin-web/src/app/App.tsx | 16 ++++++++++++++-- admin-web/src/components/Layout.tsx | 13 +++++++++++-- admin-web/src/styles/global.css | 4 +++- deploy/docker-compose.yml | 9 +++++++-- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/admin-web/src/api/client.ts b/admin-web/src/api/client.ts index 0161e7e..606fe6e 100644 --- a/admin-web/src/api/client.ts +++ b/admin-web/src/api/client.ts @@ -1,4 +1,5 @@ const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "/api/v1"; +export const AUTH_EXPIRED_EVENT = "nexavpn-admin-auth-expired"; export type User = { id: string; @@ -85,6 +86,10 @@ async function request(path: string, init?: RequestInit): Promise { }); if (!response.ok) { + if (response.status === 401) { + localStorage.removeItem("nexavpn_admin_token"); + window.dispatchEvent(new Event(AUTH_EXPIRED_EVENT)); + } throw new Error(`Request failed: ${response.status}`); } diff --git a/admin-web/src/app/App.tsx b/admin-web/src/app/App.tsx index fb6b301..738caf8 100644 --- a/admin-web/src/app/App.tsx +++ b/admin-web/src/app/App.tsx @@ -1,6 +1,7 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Navigate, Route, Routes } from "react-router-dom"; +import { AUTH_EXPIRED_EVENT } from "../api/client"; import { Layout } from "../components/Layout"; import { AuditPage } from "../features/audit/AuditPage"; import { LoginPage } from "../features/auth/LoginPage"; @@ -20,13 +21,24 @@ export function App() { setToken(accessToken); } + function handleLogout() { + localStorage.removeItem("nexavpn_admin_token"); + setToken(""); + } + + useEffect(() => { + const onExpired = () => handleLogout(); + window.addEventListener(AUTH_EXPIRED_EVENT, onExpired); + return () => window.removeEventListener(AUTH_EXPIRED_EVENT, onExpired); + }, []); + return ( : } /> - : }> + : }> } /> } /> } /> diff --git a/admin-web/src/components/Layout.tsx b/admin-web/src/components/Layout.tsx index ec332f7..e87103b 100644 --- a/admin-web/src/components/Layout.tsx +++ b/admin-web/src/components/Layout.tsx @@ -10,7 +10,11 @@ const items = [ ["Settings", "/settings"] ]; -export function Layout() { +type LayoutProps = { + onLogout: () => void; +}; + +export function Layout({ onLogout }: LayoutProps) { return (
-
Secure by design
+
+
Secure by design
+ +
diff --git a/admin-web/src/styles/global.css b/admin-web/src/styles/global.css index 051e9de..4a5b0e5 100644 --- a/admin-web/src/styles/global.css +++ b/admin-web/src/styles/global.css @@ -63,7 +63,8 @@ button { .auth-brand, .brand-block, -.topbar-brand { +.topbar-brand, +.topbar-actions { display: flex; align-items: center; gap: 16px; @@ -300,6 +301,7 @@ button { .auth-brand, .brand-block, .topbar-brand, + .topbar-actions, .page-header { align-items: flex-start; flex-direction: column; diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index b39df3e..940c614 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -18,6 +18,7 @@ services: build: context: ../backend dockerfile: Dockerfile + hostname: backend env_file: - .env depends_on: @@ -25,8 +26,12 @@ services: ports: - "8080:8080" networks: - - control - - gateway + control: + aliases: + - backend + gateway: + aliases: + - backend admin-web: build: