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: