190 lines
6.3 KiB
TypeScript
190 lines
6.3 KiB
TypeScript
import { cookies } from 'next/headers';
|
|
import { redirect } from 'next/navigation';
|
|
import { OperationsShell } from '../../components/ops/operations-shell';
|
|
import { OperationsErrorState } from '../../components/ops/operations-error-state';
|
|
import { normalizeHealthResponse, sanitizeAuditEvent } from '../../lib/ops-data';
|
|
import {
|
|
OperatorSession,
|
|
OperationsHealth,
|
|
SafeAuditEvent,
|
|
OperationsNavId,
|
|
} from '../../components/ops/types';
|
|
|
|
interface AdminPageProps {
|
|
searchParams?: Promise<{
|
|
nav?: string;
|
|
case?: string;
|
|
error?: string;
|
|
}>;
|
|
}
|
|
|
|
|
|
function parseNavParam(nav?: string): OperationsNavId {
|
|
if (nav === 'incidents' || nav === 'audit' || nav === 'operators' || nav === 'session') {
|
|
return nav;
|
|
}
|
|
return 'overview';
|
|
}
|
|
|
|
export default async function AdminPage({ searchParams }: AdminPageProps) {
|
|
const resolvedParams = searchParams ? await searchParams : {};
|
|
const initialNav = parseNavParam(resolvedParams.nav);
|
|
const initialCaseRef = typeof resolvedParams.case === 'string' ? resolvedParams.case : null;
|
|
|
|
const cookieStore = await cookies();
|
|
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_ENV === 'production';
|
|
const opsToken = isProduction
|
|
? cookieStore.get('__Host-motorsignal_ops')?.value
|
|
: cookieStore.get('__Host-motorsignal_ops')?.value ??
|
|
cookieStore.get('motorsignal_ops')?.value;
|
|
|
|
const riderToken = cookieStore.get('motorsignal_access_token')?.value;
|
|
|
|
if (resolvedParams.error === 'forbidden') {
|
|
return (
|
|
<section className="ops-error-root min-h-screen bg-surface flex items-center justify-center p-4">
|
|
<OperationsErrorState
|
|
status={403}
|
|
message="You do not have sufficient authority to access Operations."
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
// Unauthenticated - prompt for login (Zero query parameter preview/bypass)
|
|
if (!opsToken) {
|
|
if (riderToken) {
|
|
redirect('/api/ops/auth/session');
|
|
}
|
|
return (
|
|
<section className="ops-error-root min-h-screen bg-surface flex items-center justify-center p-4">
|
|
<OperationsErrorState
|
|
status={401}
|
|
message="Your Operations session has expired or is unauthenticated."
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
|
const headers = {
|
|
authorization: `Bearer ${opsToken}`,
|
|
accept: 'application/json',
|
|
};
|
|
|
|
try {
|
|
// STEP 1: Verify Operations identity & role first
|
|
const meResponse = await fetch(`${baseUrl}/api/v1/ops/me`, { headers, cache: 'no-store' });
|
|
|
|
if (meResponse.status === 401) {
|
|
if (riderToken) {
|
|
redirect('/api/ops/auth/session');
|
|
}
|
|
return (
|
|
<section className="ops-error-root min-h-screen bg-surface flex items-center justify-center p-4">
|
|
<OperationsErrorState
|
|
status={401}
|
|
message="Your Operations session has expired. Please sign in again."
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
|
|
if (meResponse.status === 403) {
|
|
return (
|
|
<section className="ops-error-root min-h-screen bg-surface flex items-center justify-center p-4">
|
|
<OperationsErrorState
|
|
status={403}
|
|
message="You do not have sufficient authority to access Operations."
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
if (!meResponse.ok) {
|
|
return (
|
|
<section className="ops-error-root min-h-screen bg-surface flex items-center justify-center p-4">
|
|
<OperationsErrorState
|
|
status={503}
|
|
message="Operations platform is temporarily unavailable."
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const meData = await meResponse.json();
|
|
const session: OperatorSession = {
|
|
identity: meData.identity ?? meData.email ?? 'operator',
|
|
role: meData.role === 'owner' ? 'owner' : 'operator',
|
|
status: meData.status === 'expired' || meData.status === 'revoked' ? meData.status : 'active',
|
|
createdAt: meData.created_at ?? meData.createdAt,
|
|
lastActivityAt: meData.last_activity_at ?? meData.lastActivityAt,
|
|
expiresAt: meData.expires_at ?? meData.expiresAt,
|
|
environment: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
|
};
|
|
|
|
// STEP 2: After successful verified session, fetch health, and ONLY fetch audit if role == 'owner'
|
|
const isOwner = session.role === 'owner';
|
|
const [healthResponse, auditResponse] = await Promise.all([
|
|
fetch(`${baseUrl}/api/v1/ops/health`, { headers, cache: 'no-store' }),
|
|
isOwner
|
|
? fetch(`${baseUrl}/api/v1/ops/audit-events`, { headers, cache: 'no-store' }).catch(() => null)
|
|
: Promise.resolve(null),
|
|
]);
|
|
|
|
let health: OperationsHealth;
|
|
if (healthResponse.ok) {
|
|
const healthData = await healthResponse.json();
|
|
health = normalizeHealthResponse(healthData);
|
|
} else {
|
|
health = normalizeHealthResponse({ status: 'unavailable', checked_at: new Date().toISOString() });
|
|
}
|
|
|
|
let initialAuditEvents: SafeAuditEvent[] = [];
|
|
let nextCursor: string | null = null;
|
|
let auditTotal: number | undefined = undefined;
|
|
|
|
if (auditResponse && auditResponse.ok) {
|
|
const auditData = await auditResponse.json();
|
|
const rawEvents = Array.isArray(auditData)
|
|
? auditData
|
|
: Array.isArray(auditData?.events)
|
|
? auditData.events
|
|
: Array.isArray(auditData?.items)
|
|
? auditData.items
|
|
: [];
|
|
initialAuditEvents = rawEvents
|
|
.map(sanitizeAuditEvent)
|
|
.filter((e: SafeAuditEvent | null): e is SafeAuditEvent => e !== null);
|
|
nextCursor = auditData?.next_cursor ?? auditData?.cursor ?? null;
|
|
if (typeof auditData?.total === 'number') {
|
|
auditTotal = auditData.total;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<OperationsShell
|
|
initialSession={session}
|
|
initialHealth={health}
|
|
initialAudit={{
|
|
events: initialAuditEvents,
|
|
next_cursor: nextCursor,
|
|
total: auditTotal,
|
|
}}
|
|
initialNav={initialNav}
|
|
initialCaseRef={initialCaseRef}
|
|
/>
|
|
);
|
|
} catch {
|
|
return (
|
|
<section className="ops-error-root min-h-screen bg-surface flex items-center justify-center p-4">
|
|
<OperationsErrorState
|
|
status={503}
|
|
message="Unable to connect to the Operations platform service."
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
}
|