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.tsrewrite proxies/api/v1/*to the gateway onlocalhost: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/jsonunless the body isFormData. - 401 handling: redirects to
/login— except on/loginand/invitepages, where a redirect would loop. - 204 handling: DELETE endpoints return no body;
requestreturnsundefinedinstead 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 cookieThe 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
- User visits a protected page (e.g.,
/compliance). - Middleware checks for
aegis_tokencookie presence. If missing, redirects to/login?redirect=/compliance. (/loginand/invite/*are public.) - Login page submits credentials via
login()→POST /api/v1/auth/token(gateway public path). - Auth service verifies the password (bcrypt) and responds with
Set-Cookie: aegis_token=…; HttpOnly; Secure; SameSite=Laxplus an identity body. setUser()hydratesAuthContextfrom the response body; later reloads hydrate fromGET /auth/me.- Subsequent API calls ride the cookie same-origin; the gateway validates the JWT locally, resolves fresh roles/permissions via
/auth/resolve, and stampsX-User-Id/X-Roles/X-Tenant-Id/X-Permissionsonto the proxied request. - 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.