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>
This commit is contained in:
109
dashboard/src/lib/auth.tsx
Normal file
109
dashboard/src/lib/auth.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user