Skip to Content

API Client

The AEGIS frontend uses a centralized API client for all backend communication. This page documents the API client, cookie-based session handling (R42b), the authentication context provider, and how requests are authenticated.

Centralized API Client

The API client is defined in src/lib/api.ts and provides a thin wrapper around fetch.

Session model (R42b)

Since R42b the session lives entirely in the httpOnly aegis_token cookie, set and cleared only by the auth service. The frontend has no token-handling code at all — no document.cookie reads or writes, no Authorization header construction. All API paths are same-origin (/api/v1/*), so the browser attaches the cookie automatically:

  • On the box, Caddy routes /api/v1/* to the Go gateway before Next.js ever sees the request.
  • In dev, a next.config.ts rewrite proxies /api/v1/* to the gateway on localhost:8000, making dev same-origin exactly like the box.
// next.config.ts async rewrites() { const gateway = process.env.GATEWAY_INTERNAL_URL || "http://localhost:8000"; return [{ source: "/api/v1/:path*", destination: `${gateway}/api/v1/:path*` }]; }

NEXT_PUBLIC_API_URL and NEXT_PUBLIC_AUTH_URL are retired.

Request Function

All API calls go through the internal request<T> function:

const API_BASE = ""; // same-origin async function request<T>(path: string, options: RequestInit = {}): Promise<T> { const headers: Record<string, string> = { ...(options.headers as Record<string, string>) }; if (options.body && !(options.body instanceof FormData)) headers["Content-Type"] = "application/json"; const res = await fetch(`${API_BASE}${path}`, { ...options, headers }); if (res.status === 401) { if (shouldRedirectOn401()) window.location.href = "/login"; throw new Error("Unauthorized"); } if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`); if (res.status === 204) return undefined as T; const text = await res.text(); return (text ? JSON.parse(text) : undefined) as T; }

Key behaviors:

  • Cookie auth: same-origin requests carry the httpOnly cookie automatically — nothing to attach.
  • Content-Type: automatically set to application/json unless the body is FormData.
  • 401 handling: redirects to /login — except on /login and /invite pages, where a redirect would loop.
  • 204 handling: DELETE endpoints return no body; request returns undefined instead of throwing on empty JSON.

Public API

export const api = { get: <T>(path: string) => request<T>(path), post: <T>(path: string, body?: unknown) => request<T>(path, { method: "POST", body: body ? JSON.stringify(body) : undefined }), put: <T>(path: string, body?: unknown) => request<T>(path, { method: "PUT", body: body ? JSON.stringify(body) : undefined }), del: <T>(path: string) => request<T>(path, { method: "DELETE" }), };

Session helpers

src/lib/api.ts also exports the four session functions the auth context uses:

login(email, password) // POST /api/v1/auth/token — session arrives as Set-Cookie; // the body carries {user_id, roles, email, display_name} only fetchMe() // GET /api/v1/auth/me — the UI's identity source (null = not logged in) refreshSession() // POST /api/v1/auth/refresh — sliding refresh, fire-and-forget serverLogout() // POST /api/v1/auth/logout — the only way to clear the httpOnly cookie

The login response body contains no access_token since R42b — the token rides only in the Set-Cookie header. Programmatic clients (smoke/golden harnesses) capture the cookie value from the response headers.

Service URL Configuration

There is none anymore. As of R42c, every frontend network call is same-origin /api/v1/* routed through the authenticated gateway — lib/api-urls.ts and all NEXT_PUBLIC_*_URL service-port env vars are deleted. Calls use either the typed api. helper or a plain same-origin fetch("/api/v1/…") (for streaming, FormData, and graceful-degradation reads where the helper’s throw-and-redirect-on-401 isn’t wanted). The gateway maps each /api/v1/* path to the right backend (e.g. /api/v1/entities → KG /managed-entities, /api/v1/graph/entities/{label} → KG raw label lists, /api/v1/detection-rules → KG /event-detection-rules, /api/v1/event-types → flaring). The one graph-query call-site uses the template-only /api/v1/graph/query-template (raw Cypher /query is never exposed).

Auth Context Provider

The AuthProvider in src/lib/auth-context.tsx provides user state to the entire application:

interface SessionUser { user_id: string; roles: string[]; email?: string; display_name?: string | null; permissions?: string[]; }

Hydration and refresh

On mount the provider hydrates identity from the server (the httpOnly cookie is not JS-readable, so GET /auth/me is the single source of “who am I”) and wires the sliding session refresh:

useEffect(() => { let cancelled = false; fetchMe().then((me) => { if (!cancelled && me) setUserState(me); }); refreshSession(); // on mount… const timer = setInterval(refreshSession, 30 * 60 * 1000); // …and every 30 min return () => { cancelled = true; clearInterval(timer); }; }, []);

There is no localStorage persistence — a reload re-hydrates from /auth/me.

Logout

const logout = () => { void serverLogout().finally(() => { setUserState(null); window.location.href = "/login"; }); };

Authentication Flow

  1. User visits a protected page (e.g., /compliance).
  2. Middleware checks for aegis_token cookie presence. If missing, redirects to /login?redirect=/compliance. (/login and /invite/* are public.)
  3. Login page submits credentials via login()POST /api/v1/auth/token (gateway public path).
  4. Auth service verifies the password (bcrypt) and responds with Set-Cookie: aegis_token=…; HttpOnly; Secure; SameSite=Lax plus an identity body.
  5. setUser() hydrates AuthContext from the response body; later reloads hydrate from GET /auth/me.
  6. Subsequent API calls ride the cookie same-origin; the gateway validates the JWT locally, resolves fresh roles/permissions via /auth/resolve, and stamps X-User-Id / X-Roles / X-Tenant-Id / X-Permissions onto the proxied request.
  7. On 401, the client redirects to /login.

Invite redemption

/invite/[token] is a public page (R42b): it validates the token via GET /api/v1/auth/invites/{token}, collects a password, and redeems via POST /api/v1/auth/invites/redeem — the redeem response sets the session cookie, logging the new user straight in.

The middleware only checks for cookie presence — it does not validate the JWT. A user with an expired or revoked token will pass the middleware but receive 401 errors from the API Gateway, which triggers the automatic redirect to /login.

Last updated on