- 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
4.1 KiB
TypeScript
110 lines
4.1 KiB
TypeScript
import * as React from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useAuth } from '@/lib/auth';
|
|
|
|
/**
|
|
* Login page — accepts API Base URL, Client ID, and Client Secret.
|
|
* Validates credentials against the AgentIdP token endpoint before persisting.
|
|
*/
|
|
export default function Login(): React.JSX.Element {
|
|
const { login } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
const [baseUrl, setBaseUrl] = React.useState<string>(window.location.origin);
|
|
const [clientId, setClientId] = React.useState<string>('');
|
|
const [clientSecret, setClientSecret] = React.useState<string>('');
|
|
const [loading, setLoading] = React.useState<boolean>(false);
|
|
const [error, setError] = React.useState<string | null>(null);
|
|
|
|
const handleSubmit = React.useCallback(
|
|
async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
|
e.preventDefault();
|
|
setError(null);
|
|
setLoading(true);
|
|
|
|
try {
|
|
const success = await login({ baseUrl: baseUrl.trim(), clientId: clientId.trim(), clientSecret });
|
|
if (success) {
|
|
navigate('/dashboard/agents', { replace: true });
|
|
} else {
|
|
setError('Invalid credentials. Please check your Client ID and secret.');
|
|
setClientSecret('');
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[login, navigate, baseUrl, clientId, clientSecret],
|
|
);
|
|
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center bg-slate-50 px-4">
|
|
<div className="w-full max-w-md rounded-xl bg-white p-8 shadow-lg">
|
|
<div className="mb-8 text-center">
|
|
<h1 className="text-2xl font-bold text-brand-700">SentryAgent.ai</h1>
|
|
<p className="mt-1 text-sm text-slate-500">AgentIdP Dashboard — Sign In</p>
|
|
</div>
|
|
|
|
<form onSubmit={(e) => { void handleSubmit(e); }} className="space-y-5">
|
|
<div>
|
|
<label htmlFor="baseUrl" className="block text-sm font-medium text-slate-700">
|
|
API Base URL
|
|
</label>
|
|
<input
|
|
id="baseUrl"
|
|
type="url"
|
|
required
|
|
value={baseUrl}
|
|
onChange={(e) => { setBaseUrl(e.target.value); }}
|
|
className="mt-1 block w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
|
|
placeholder="https://api.example.com"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="clientId" className="block text-sm font-medium text-slate-700">
|
|
Client ID
|
|
</label>
|
|
<input
|
|
id="clientId"
|
|
type="text"
|
|
required
|
|
value={clientId}
|
|
onChange={(e) => { setClientId(e.target.value); }}
|
|
className="mt-1 block w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
|
|
placeholder="agent-uuid"
|
|
autoComplete="username"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="clientSecret" className="block text-sm font-medium text-slate-700">
|
|
Client Secret
|
|
</label>
|
|
<input
|
|
id="clientSecret"
|
|
type="password"
|
|
required
|
|
value={clientSecret}
|
|
onChange={(e) => { setClientSecret(e.target.value); }}
|
|
className="mt-1 block w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
|
|
autoComplete="current-password"
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="rounded-md bg-red-50 px-3 py-2 text-sm text-red-700" role="alert">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<Button type="submit" loading={loading} className="w-full" size="lg">
|
|
{loading ? 'Validating…' : 'Sign In'}
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|