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:
SentryAgent.ai Developer
2026-04-04 02:20:09 +00:00
parent 0fad328329
commit eea885db04
34 changed files with 4262 additions and 25 deletions

137
portal/app/login/page.tsx Normal file
View 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>
);
}