- dashboard/: Vite 5 + React 18 + TypeScript strict SPA
- Auth: sessionStorage credentials, TokenManager validation, AuthProvider context
- Pages: Login, Agents (search + filter), AgentDetail (suspend/reactivate),
Credentials (generate/rotate/revoke, new secret shown once),
AuditLog (filters + pagination), Health (PG + Redis status, 30s refresh)
- Components: Button, Badge, ConfirmDialog, AppShell, RequireAuth
- All destructive actions gated by ConfirmDialog
- Zero dangerouslySetInnerHTML; sessionStorage only (OWASP compliant)
- src/routes/health.ts: unauthenticated GET /health — PG + Redis connectivity
- src/app.ts: health route + dashboard/dist/ served at /dashboard with SPA fallback
- 6 new health route tests; 308/308 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
110 lines
3.1 KiB
TypeScript
110 lines
3.1 KiB
TypeScript
import { TokenManager } from '@sentryagent/idp-sdk';
|
|
|
|
const SESSION_KEY = 'agentidp_credentials';
|
|
|
|
interface StoredCredentials {
|
|
clientId: string;
|
|
clientSecret: string;
|
|
baseUrl: string;
|
|
}
|
|
|
|
/**
|
|
* Persists user credentials to sessionStorage (cleared on tab close).
|
|
*/
|
|
export function saveCredentials(creds: StoredCredentials): void {
|
|
sessionStorage.setItem(SESSION_KEY, JSON.stringify(creds));
|
|
}
|
|
|
|
/**
|
|
* Retrieves credentials from sessionStorage.
|
|
* Returns null if not logged in.
|
|
*/
|
|
export function loadCredentials(): StoredCredentials | null {
|
|
const raw = sessionStorage.getItem(SESSION_KEY);
|
|
if (!raw) return null;
|
|
try {
|
|
return JSON.parse(raw) as StoredCredentials;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes credentials from sessionStorage (logout).
|
|
*/
|
|
export function clearCredentials(): void {
|
|
sessionStorage.removeItem(SESSION_KEY);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the user has stored credentials.
|
|
*/
|
|
export function isAuthenticated(): boolean {
|
|
return loadCredentials() !== null;
|
|
}
|
|
|
|
/**
|
|
* Validates stored credentials by requesting a token.
|
|
* Returns true if successful; false on auth failure.
|
|
*/
|
|
export async function validateCredentials(creds: StoredCredentials): Promise<boolean> {
|
|
try {
|
|
const tm = new TokenManager(creds.baseUrl, creds.clientId, creds.clientSecret, 'agents:read agents:write tokens:read audit:read');
|
|
await tm.getToken();
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ── React context ──────────────────────────────────────────────────────────────
|
|
|
|
import * as React from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
interface AuthContextValue {
|
|
credentials: StoredCredentials | null;
|
|
login: (creds: StoredCredentials) => Promise<boolean>;
|
|
logout: () => void;
|
|
}
|
|
|
|
const AuthContext = React.createContext<AuthContextValue | null>(null);
|
|
|
|
/**
|
|
* Provides authentication state to the application.
|
|
* Reads initial state from sessionStorage on mount.
|
|
*/
|
|
export function AuthProvider({ children }: { children: React.ReactNode }): React.JSX.Element {
|
|
const [credentials, setCredentials] = React.useState<StoredCredentials | null>(loadCredentials);
|
|
const navigate = useNavigate();
|
|
|
|
const login = React.useCallback(async (creds: StoredCredentials): Promise<boolean> => {
|
|
const valid = await validateCredentials(creds);
|
|
if (valid) {
|
|
saveCredentials(creds);
|
|
setCredentials(creds);
|
|
}
|
|
return valid;
|
|
}, []);
|
|
|
|
const logout = React.useCallback((): void => {
|
|
clearCredentials();
|
|
setCredentials(null);
|
|
navigate('/dashboard/login');
|
|
}, [navigate]);
|
|
|
|
const value = React.useMemo(() => ({ credentials, login, logout }), [credentials, login, logout]);
|
|
|
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
}
|
|
|
|
/**
|
|
* Returns the current authentication context.
|
|
* Must be used inside <AuthProvider>.
|
|
*/
|
|
export function useAuth(): AuthContextValue {
|
|
const ctx = React.useContext(AuthContext);
|
|
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
return ctx;
|
|
}
|