Files
sentryagent-idp/dashboard/src/lib/auth.tsx
SentryAgent.ai Developer 7d6e248a14 feat(phase-2): workstream 6 — Web Dashboard UI
- 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>
2026-03-28 23:19:18 +00:00

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;
}