feat(phase-6): WS3+WS4+WS6 — Analytics, API Tiers, AGNTCY Compliance
WS3 — Advanced Analytics Dashboard: - DB migration: analytics_events table (tenant_id, date, metric_type, count) - AnalyticsService: recordEvent (fire-and-forget), getTokenTrend, getAgentActivity, getAgentUsageSummary - Analytics hooks in OAuth2Service (token_issued) and AgentService (agent_registered/deactivated) - AnalyticsController + routes/analytics.ts (gated by ANALYTICS_ENABLED flag) - Portal: TokenTrendChart (recharts LineChart), AgentHeatmap (recharts heatmap), /analytics page WS4 — API Gateway Tiers: - DB migration: tenant_tiers table; src/config/tiers.ts (free/pro/enterprise limits) - TierService: getStatus, initiateUpgrade (Stripe), applyUpgrade; TierLimitError in errors.ts - tierEnforcement middleware (Redis-backed daily call/token counters; TIER_ENFORCEMENT flag) - Agent count enforcement in AgentService.create() - Stripe webhook updated to call TierService.applyUpgrade() on checkout.session.completed - TierController + routes/tiers.ts; Portal: /settings/tier page with upgrade flow WS6 — AGNTCY Compliance Certification: - ComplianceService: generateReport() (Redis-cached 5 min), exportAgentCards() - Compliance sections: agent-identity (DID + credential expiry checks), audit-trail (Merkle chain) - ComplianceController updated with getComplianceReport, exportAgentCards handlers - routes/compliance.ts: new AGNTCY routes (gated by COMPLIANCE_ENABLED flag); SOC2 routes unaffected QA: - 28 new unit tests: AnalyticsService (8), TierService (9), ComplianceService (11) — all pass - 673 total unit tests passing; 0 TypeScript errors across API and portal - AGNTCY conformance test suite at tests/agntcy-conformance/ (4 protocol tests) - Portal builds cleanly: 9 routes including /analytics and /settings/tier - Feature flags verified: ANALYTICS_ENABLED, TIER_ENFORCEMENT, COMPLIANCE_ENABLED Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
137
portal/app/login/page.tsx
Normal file
137
portal/app/login/page.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* LoginPage — Tenant admin sign-in page for the SentryAgent developer portal.
|
||||
*
|
||||
* Posts credentials to `POST /api/tenants/login` on the AgentIdP backend and
|
||||
* stores the returned JWT in localStorage so that protected pages (e.g.
|
||||
* /analytics) can read it via the `useAuth` hook.
|
||||
*
|
||||
* @module app/login/page
|
||||
*/
|
||||
|
||||
import React, { useState, type FormEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AUTH_TOKEN_KEY } from '@/hooks/useAuth';
|
||||
|
||||
/** Shape of the successful login response from the AgentIdP API. */
|
||||
interface LoginResponse {
|
||||
access_token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the portal login form and handles credential submission.
|
||||
*
|
||||
* @returns JSX element
|
||||
*/
|
||||
export default function LoginPage(): React.ReactElement {
|
||||
const router = useRouter();
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000';
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
/**
|
||||
* Submits the login form and stores the JWT on success.
|
||||
*
|
||||
* @param e - The form submission event
|
||||
*/
|
||||
async function handleSubmit(e: FormEvent<HTMLFormElement>): Promise<void> {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${apiUrl}/api/tenants/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = (await res.json().catch(() => ({}))) as { message?: string };
|
||||
setError(body.message ?? 'Invalid credentials. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = (await res.json()) as LoginResponse;
|
||||
localStorage.setItem(AUTH_TOKEN_KEY, data.access_token);
|
||||
router.replace('/analytics');
|
||||
} catch {
|
||||
setError('Network error. Please check your connection and try again.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[70vh] items-center justify-center px-6 py-16">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-3xl font-extrabold text-slate-900">Sign in</h1>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Access your SentryAgent tenant dashboard
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => void handleSubmit(e)}
|
||||
className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="mb-1.5 block text-sm font-medium text-slate-700"
|
||||
>
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
autoComplete="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
|
||||
placeholder="admin@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="mb-1.5 block text-sm font-medium text-slate-700"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
autoComplete="current-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error !== null && (
|
||||
<p className="mb-4 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="w-full rounded-lg bg-brand-600 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-brand-700 disabled:opacity-60"
|
||||
>
|
||||
{submitting ? 'Signing in…' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user