diff --git a/website/app/admin/page.tsx b/website/app/admin/page.tsx index 6b9c3c7..4c059a8 100644 --- a/website/app/admin/page.tsx +++ b/website/app/admin/page.tsx @@ -1,26 +1,163 @@ -import Link from 'next/link'; import { cookies } from 'next/headers'; -import { AdminConsole } from '../../components/admin-console'; +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'; -type AuditEvent = { id: string; event_type: string; actor_user_id: string | null; target_user_id: string | null; created_at: string; metadata: Record | null }; +interface AdminPageProps { + searchParams?: Promise<{ + nav?: string; + }>; +} + +function parseNavParam(nav?: string): OperationsNavId { + if (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 cookieStore = await cookies(); + const isProduction = process.env.NODE_ENV === 'production'; + const opsToken = isProduction + ? cookieStore.get('__Host-motorsignal_ops')?.value + : cookieStore.get('__Host-motorsignal_ops')?.value ?? + cookieStore.get('motorsignal_ops')?.value; + + // Unauthenticated - prompt for login (Zero query parameter preview/bypass) + if (!opsToken) { + return ( +
+ +
+ ); + } -async function loadAuditEvents(): Promise<{ status: number; events: AuditEvent[] }> { - const token = (await cookies()).get('motorsignal_access_token')?.value; - if (!token) return { status: 401, events: [] }; const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app'; + const headers = { + authorization: `Bearer ${opsToken}`, + accept: 'application/json', + }; + try { - const response = await fetch(`${baseUrl}/api/v1/admin/audit-events`, { headers: { authorization: `Bearer ${token}` }, cache: 'no-store' }); - if (!response.ok) return { status: response.status, events: [] }; - return { status: response.status, events: await response.json() as AuditEvent[] }; + // STEP 1: Verify Operations identity & role first + const meResponse = await fetch(`${baseUrl}/api/v1/ops/me`, { headers, cache: 'no-store' }); + + if (meResponse.status === 401) { + return ( +
+ +
+ ); + } + + if (meResponse.status === 403) { + return ( +
+ +
+ ); + } + + if (!meResponse.ok) { + return ( +
+ +
+ ); + } + + 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 ( + + ); } catch { - return { status: 503, events: [] }; + return ( +
+ +
+ ); } } - -export default async function AdminPage() { - const result = await loadAuditEvents(); - if (result.status === 401) return
Admin console

Sign in required.

Admin tools require an authenticated MotorSignal session.

Sign in
; - if (result.status === 403) return
Admin console

Access restricted.

Your account does not have support or admin permissions.

Back to account
; - if (result.status !== 200) return
Admin console

Console unavailable.

Audit data could not be loaded. Try again when the API is available.

; - return
Admin console

Operations.

Manage users, vehicles, rides, and review recent account activity.

Recent audit events

{result.events.length === 0 ?

No events recorded.

: result.events.map((event) =>
{event.event_type}

{event.actor_user_id ?? 'System'} {event.target_user_id ? `targeted ${event.target_user_id}` : ''}

)}
; -} diff --git a/website/app/api/admin/[...path]/route.ts b/website/app/api/admin/[...path]/route.ts deleted file mode 100644 index 69ccc0d..0000000 --- a/website/app/api/admin/[...path]/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { cookies } from 'next/headers'; -import { NextRequest, NextResponse } from 'next/server'; - -const resources = new Set(['users', 'vehicles', 'rides']); - -async function proxy(request: NextRequest, context: { params: Promise<{ path: string[] }> }) { - const { path } = await context.params; - if (!path.length || !resources.has(path[0])) return NextResponse.json({ detail: 'Unknown admin resource.' }, { status: 404 }); - const token = (await cookies()).get('motorsignal_access_token')?.value; - if (!token) return NextResponse.json({ detail: 'Authentication required.' }, { status: 401 }); - const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app'; - const body = request.method === 'GET' || request.method === 'DELETE' ? undefined : await request.text(); - const response = await fetch(`${baseUrl}/api/v1/admin/${path.join('/')}${request.nextUrl.search}`, { method: request.method, headers: { authorization: `Bearer ${token}`, ...(body ? { 'content-type': 'application/json' } : {}) }, body, cache: 'no-store' }); - return new NextResponse(await response.text(), { status: response.status, headers: { 'content-type': response.headers.get('content-type') ?? 'application/json' } }); -} - -export const GET = proxy; -export const POST = proxy; -export const PATCH = proxy; -export const DELETE = proxy; diff --git a/website/app/api/ops/audit-events/route.ts b/website/app/api/ops/audit-events/route.ts new file mode 100644 index 0000000..6461c6e --- /dev/null +++ b/website/app/api/ops/audit-events/route.ts @@ -0,0 +1,87 @@ +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { sanitizeAuditEvent } from '../../../../lib/ops-data'; +import { SafeAuditEvent } from '../../../../components/ops/types'; + +export async function GET(request: NextRequest) { + const cookieStore = await cookies(); + const isProduction = process.env.NODE_ENV === 'production'; + const opsToken = isProduction + ? cookieStore.get('__Host-motorsignal_ops')?.value + : cookieStore.get('__Host-motorsignal_ops')?.value ?? + cookieStore.get('motorsignal_ops')?.value; + + if (!opsToken) { + return NextResponse.json( + { detail: 'Authentication required.' }, + { status: 401 } + ); + } + + const cursor = request.nextUrl.searchParams.get('cursor'); + const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app'; + const targetUrl = new URL(`${baseUrl}/api/v1/ops/audit-events`); + if (cursor) { + targetUrl.searchParams.set('cursor', cursor); + } + + try { + const response = await fetch(targetUrl.toString(), { + method: 'GET', + headers: { + authorization: `Bearer ${opsToken}`, + accept: 'application/json', + }, + cache: 'no-store', + }); + + if (response.status === 401) { + return NextResponse.json( + { detail: 'Your Operations session has expired.' }, + { status: 401 } + ); + } + if (response.status === 403) { + return NextResponse.json( + { detail: 'You do not have authority to view Audit.' }, + { status: 403 } + ); + } + if (!response.ok) { + return NextResponse.json( + { detail: 'Operations is temporarily unavailable.' }, + { status: response.status >= 500 ? response.status : 503 } + ); + } + + const data = await response.json(); + const rawEvents = Array.isArray(data) + ? data + : Array.isArray(data?.events) + ? data.events + : Array.isArray(data?.items) + ? data.items + : []; + + const safeEvents: SafeAuditEvent[] = rawEvents + .map(sanitizeAuditEvent) + .filter((e: SafeAuditEvent | null): e is SafeAuditEvent => e !== null); + + const nextCursor = data?.next_cursor ?? data?.cursor ?? null; + const total = typeof data?.total === 'number' ? data.total : undefined; + + return NextResponse.json( + { + events: safeEvents, + next_cursor: nextCursor, + total, + }, + { status: 200 } + ); + } catch { + return NextResponse.json( + { detail: 'Operations is temporarily unavailable.' }, + { status: 503 } + ); + } +} diff --git a/website/app/api/ops/auth/logout/route.ts b/website/app/api/ops/auth/logout/route.ts new file mode 100644 index 0000000..3efce38 --- /dev/null +++ b/website/app/api/ops/auth/logout/route.ts @@ -0,0 +1,61 @@ +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { hasValidOrigin } from '../../../../../lib/request-origin'; + +export async function POST(request: NextRequest) { + // 1. Validate request origin + if (!hasValidOrigin(request)) { + return NextResponse.json({ detail: 'Invalid request origin.' }, { status: 403 }); + } + + // 2. Obtain real Operations session cookie + const cookieStore = await cookies(); + const isProduction = process.env.NODE_ENV === 'production'; + const opsToken = isProduction + ? cookieStore.get('__Host-motorsignal_ops')?.value + : cookieStore.get('__Host-motorsignal_ops')?.value ?? + cookieStore.get('motorsignal_ops')?.value; + + // 3. If token present, invoke backend session revocation endpoint + if (opsToken) { + const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app'; + try { + await fetch(`${baseUrl}/api/v1/ops/sessions/current`, { + method: 'DELETE', + headers: { + authorization: `Bearer ${opsToken}`, + accept: 'application/json', + }, + cache: 'no-store', + }); + // All terminal backend states (204, 200, 401 already expired, 403, 404, 503) proceed to clear local cookie + } catch { + // Network failure / backend unavailable: proceed safely to clear local cookie without leaking internals + } + } + + // 4. Clear local Operations cookies (host and non-host dev fallback) + const response = new NextResponse(null, { status: 204 }); + + response.cookies.set('__Host-motorsignal_ops', '', { + httpOnly: true, + secure: true, + sameSite: 'strict', + path: '/', + maxAge: 0, + expires: new Date(0), + }); + + response.cookies.set('motorsignal_ops', '', { + httpOnly: true, + secure: isProduction, + sameSite: 'strict', + path: '/', + maxAge: 0, + expires: new Date(0), + }); + + // Rider consumer session cookies are completely unreferenced and untouched + + return response; +} diff --git a/website/app/api/ops/health/route.ts b/website/app/api/ops/health/route.ts new file mode 100644 index 0000000..61959d5 --- /dev/null +++ b/website/app/api/ops/health/route.ts @@ -0,0 +1,56 @@ +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { normalizeHealthResponse } from '../../../../lib/ops-data'; + +export async function GET(request: NextRequest) { + const cookieStore = await cookies(); + const isProduction = process.env.NODE_ENV === 'production'; + const opsToken = isProduction + ? cookieStore.get('__Host-motorsignal_ops')?.value + : cookieStore.get('__Host-motorsignal_ops')?.value ?? + cookieStore.get('motorsignal_ops')?.value; + + const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app'; + const headers: Record = { + accept: 'application/json', + }; + + if (opsToken) { + headers['authorization'] = `Bearer ${opsToken}`; + } + + try { + const response = await fetch(`${baseUrl}/api/v1/ops/health`, { + method: 'GET', + headers, + cache: 'no-store', + }); + + if (!response.ok) { + if (response.status === 401) { + return NextResponse.json( + { detail: 'Authentication required.' }, + { status: 401 } + ); + } + if (response.status === 403) { + return NextResponse.json( + { detail: 'Access restricted.' }, + { status: 403 } + ); + } + return NextResponse.json( + normalizeHealthResponse({ status: 'unavailable', checked_at: new Date().toISOString() }), + { status: 503 } + ); + } + + const data = await response.json(); + return NextResponse.json(normalizeHealthResponse(data), { status: 200 }); + } catch { + return NextResponse.json( + normalizeHealthResponse({ status: 'unavailable', checked_at: new Date().toISOString() }), + { status: 503 } + ); + } +} diff --git a/website/app/api/ops/session/route.ts b/website/app/api/ops/session/route.ts new file mode 100644 index 0000000..d1037f4 --- /dev/null +++ b/website/app/api/ops/session/route.ts @@ -0,0 +1,68 @@ +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; +import { OperatorSession } from '../../../../components/ops/types'; + +export async function GET(request: NextRequest) { + const cookieStore = await cookies(); + const isProduction = process.env.NODE_ENV === 'production'; + const opsToken = isProduction + ? cookieStore.get('__Host-motorsignal_ops')?.value + : cookieStore.get('__Host-motorsignal_ops')?.value ?? + cookieStore.get('motorsignal_ops')?.value; + + if (!opsToken) { + return NextResponse.json( + { detail: 'Authentication required.' }, + { status: 401 } + ); + } + + const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app'; + try { + const response = await fetch(`${baseUrl}/api/v1/ops/me`, { + method: 'GET', + headers: { + authorization: `Bearer ${opsToken}`, + accept: 'application/json', + }, + cache: 'no-store', + }); + + if (response.status === 401) { + return NextResponse.json( + { detail: 'Your Operations session has expired.' }, + { status: 401 } + ); + } + if (response.status === 403) { + return NextResponse.json( + { detail: 'Access restricted.' }, + { status: 403 } + ); + } + if (!response.ok) { + return NextResponse.json( + { detail: 'Operations is temporarily unavailable.' }, + { status: 503 } + ); + } + + const data = await response.json(); + const session: OperatorSession = { + identity: data.identity ?? data.email ?? 'operator', + role: data.role === 'owner' ? 'owner' : 'operator', + status: data.status === 'expired' || data.status === 'revoked' ? data.status : 'active', + createdAt: data.created_at ?? data.createdAt, + lastActivityAt: data.last_activity_at ?? data.lastActivityAt, + expiresAt: data.expires_at ?? data.expiresAt, + environment: process.env.NODE_ENV === 'production' ? 'production' : 'development', + }; + + return NextResponse.json({ session }, { status: 200 }); + } catch { + return NextResponse.json( + { detail: 'Operations is temporarily unavailable.' }, + { status: 503 } + ); + } +} diff --git a/website/components/admin-console.tsx b/website/components/admin-console.tsx deleted file mode 100644 index 623dc4c..0000000 --- a/website/components/admin-console.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; - -import { FormEvent, useEffect, useState } from 'react'; - -type Resource = 'users' | 'vehicles' | 'rides'; -type Item = Record; - -async function adminRequest(path: string, init?: RequestInit) { - const response = await fetch(`/api/admin/${path}`, { ...init, headers: { 'content-type': 'application/json', ...(init?.headers ?? {}) } }); - const body = await response.json().catch(() => null); - if (!response.ok) throw new Error(body?.detail ?? 'Admin request failed.'); - return body; -} - -export function AdminConsole() { - const [resource, setResource] = useState('users'); - const [items, setItems] = useState([]); - const [message, setMessage] = useState(''); - const [busy, setBusy] = useState(false); - - async function load(selected = resource) { - setBusy(true); setMessage(''); - try { const body = await adminRequest(`${selected}?page=1&page_size=50`); setItems(body.items ?? []); } - catch (error) { setItems([]); setMessage(error instanceof Error ? error.message : 'Unable to load admin data.'); } - finally { setBusy(false); } - } - - useEffect(() => { void load(); }, [resource]); - - async function create(event: FormEvent) { - event.preventDefault(); setBusy(true); setMessage(''); - const form = new FormData(event.currentTarget); - const payload = Object.fromEntries(form.entries()); - try { await adminRequest(resource, { method: 'POST', body: JSON.stringify(payload) }); event.currentTarget.reset(); setMessage('Created.'); await load(); } - catch (error) { setMessage(error instanceof Error ? error.message : 'Create failed.'); } - finally { setBusy(false); } - } - - async function archive(id: string) { - setBusy(true); setMessage(''); - try { await adminRequest(`${resource}/${id}`, { method: 'DELETE' }); setMessage(resource === 'rides' ? 'Ride archived.' : 'Deleted.'); await load(); } - catch (error) { setMessage(error instanceof Error ? error.message : 'Action failed.'); } - finally { setBusy(false); } - } - - return
{(['users', 'vehicles', 'rides'] as Resource[]).map((value) => )}
{resource === 'users' && <>}{resource === 'vehicles' && <>}{resource === 'rides' && <>}
{message &&

{message}

}
{resource === 'users' ? <>{items.map((item) => {resource === 'users' ? <> : resource === 'vehicles' ? <> : <>})}
EmailRoleStatus : resource === 'vehicles' ? <>NicknameMake / modelOwner : <>RideRiderDistanceStatus}
{String(item.email)}{String(item.role)}{item.is_active ? 'Active' : 'Inactive'}{String(item.nickname)}{String(item.make)} {String(item.model)}{String(item.user_id)}{String(item.id).slice(0, 8)}{String(item.user_id)}{item.distance_miles == null ? '-' : `${item.distance_miles} mi`}{String(item.status)}{item.status !== 'archived' && }
{!busy && items.length === 0 &&

No {resource} found.

}
; -} diff --git a/website/components/ops/operations-audit.tsx b/website/components/ops/operations-audit.tsx new file mode 100644 index 0000000..94b1262 --- /dev/null +++ b/website/components/ops/operations-audit.tsx @@ -0,0 +1,254 @@ +'use client'; + +import React from 'react'; +import { SafeAuditEvent, OperationsRole } from './types'; + +interface OperationsAuditProps { + role: OperationsRole; + events: SafeAuditEvent[]; + nextCursor?: string | null; + onNextPage?: () => Promise | void; + isLoading?: boolean; + isLoadingMore?: boolean; + total?: number; +} + +export function OperationsAudit({ + role, + events, + nextCursor, + onNextPage, + isLoading = false, + isLoadingMore = false, + total, +}: OperationsAuditProps) { + const isOwner = role === 'owner'; + + // Section 7: Operator Denied Access UX + if (!isOwner) { + return ( +
+
+ lock +
+
+ + ACCESS RESTRICTED + +

+ Owner Authority Required +

+

+ You do not have authority to view Audit. Audit log inspection is restricted to verified Operations Owners. +

+
+
+ Current Role: + + {role} + +
+
+ ); + } + + return ( +
+ {/* Header Row */} +
+
+ + GOVERNANCE & COMPLIANCE + +

+ Audit Activity +

+

+ Immutable operational and security activity log across platform boundaries. +

+
+ + {total !== undefined && ( +
+ + Recorded Events + + + {total.toLocaleString()} + +
+ )} +
+ + {/* Audit Activity Table Container */} +
+
+ + + + + + + + + + + + + {isLoading ? ( + // Skeleton loading state + Array.from({ length: 5 }).map((_, index) => ( + + + + + + + + + )) + ) : events.length === 0 ? ( + + + + ) : ( + events.map((event, index) => { + const formattedTime = new Date(event.timestamp).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + + const isSuccess = + event.result.toLowerCase() === 'success' || + event.result.toLowerCase() === 'recorded'; + const isFailure = + event.result.toLowerCase() === 'failure' || + event.result.toLowerCase() === 'denied'; + + return ( + + {/* TIME */} + + + {/* ACTION */} + + + {/* RESULT */} + + + {/* ACTOR (Current Operations Identity) */} + + + {/* TARGET */} + + + {/* CASE */} + + + ); + }) + )} + +
+ TIME + + ACTION + + RESULT + + ACTOR + + TARGET + + CASE +
+
+
+
+
+
+
+
+
+
+
+
+
+ + inbox + +

No audit events recorded.

+
+ + +
+ {event.action} + {event.reason_code && ( + + reason: {event.reason_code} + + )} +
+
+ + {event.result} + + +
+ + {event.actor_identity} + + {event.actor_role && ( + + Role: {event.actor_role} + + )} +
+
+ {event.target_resource || '--'} + + {event.case_reference || '--'} +
+
+ + {/* Server Cursor Bounded Pagination */} + {nextCursor && ( +
+ + Displaying bounded result set + + +
+ )} +
+
+ ); +} diff --git a/website/components/ops/operations-error-state.tsx b/website/components/ops/operations-error-state.tsx new file mode 100644 index 0000000..f1f0527 --- /dev/null +++ b/website/components/ops/operations-error-state.tsx @@ -0,0 +1,97 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; + +interface OperationsErrorStateProps { + status: 401 | 403 | 404 | 503 | 'disabled' | 'network' | number; + message?: string; + onRetry?: () => void; +} + +export function OperationsErrorState({ + status, + message, + onRetry, +}: OperationsErrorStateProps) { + let title = 'Operations Error'; + let description = message ?? 'An unexpected error occurred in the Operations console.'; + let icon = 'error'; + let showSignIn = false; + let showRetry = true; + + if (status === 401) { + title = 'Authentication Required'; + description = message ?? 'Your Operations session has expired or is unauthenticated.'; + icon = 'lock'; + showSignIn = true; + showRetry = false; + } else if (status === 403) { + title = 'Access Restricted'; + description = message ?? 'You do not have sufficient authority to access Operations.'; + icon = 'gavel'; + showSignIn = false; + showRetry = false; + } else if (status === 404) { + title = 'Resource Not Found'; + description = message ?? 'The requested Operations resource does not exist.'; + icon = 'search_off'; + } else if (status === 503 || status === 'network') { + title = 'Operations Temporarily Unavailable'; + description = message ?? 'Unable to communicate with the Operations API. Try again in a few moments.'; + icon = 'cloud_off'; + } else if (status === 'disabled') { + title = 'Operations Disabled'; + description = message ?? 'The Operations platform is currently disabled by system policy.'; + icon = 'block'; + showRetry = false; + } + + return ( +
+
+ {icon} +
+ +
+ + STATUS {typeof status === 'number' ? status : status.toUpperCase()} + +

{title}

+

+ {description} +

+
+ +
+ {showSignIn && ( + + login + Sign In + + )} + + {showRetry && onRetry && ( + + )} + + + Return Home + +
+
+ ); +} diff --git a/website/components/ops/operations-operators.tsx b/website/components/ops/operations-operators.tsx new file mode 100644 index 0000000..89d930b --- /dev/null +++ b/website/components/ops/operations-operators.tsx @@ -0,0 +1,50 @@ +'use client'; + +import React from 'react'; + +export function OperationsOperators() { + return ( +
+ {/* Header Row */} +
+ + ACCESS CONTROL + +

+ OPERATORS +

+

+ Directory and role assignments for platform administrative operators. +

+
+ + {/* Clean Placeholder Panel */} +
+
+ manage_accounts +
+ +
+

+ OPERATORS +

+

+ Operator management is not enabled in this Operations foundation. +

+
+ +
+
+ + info + + Security Governance Policy +
+

+ Operator accounts, role promotions, and revocations are managed strictly through versioned PostgreSQL infrastructure migrations on the ops.operators table. +

+
+
+
+ ); +} diff --git a/website/components/ops/operations-overview.tsx b/website/components/ops/operations-overview.tsx new file mode 100644 index 0000000..a52b352 --- /dev/null +++ b/website/components/ops/operations-overview.tsx @@ -0,0 +1,203 @@ +'use client'; + +import React from 'react'; +import { OperationsHealth, ComponentHealthStatus, HealthComponentKey } from './types'; +import { HEALTH_COMPONENT_METADATA } from '../../lib/ops-data'; + +interface OperationsOverviewProps { + health: OperationsHealth; + onRefresh: () => Promise | void; + isRefreshing: boolean; +} + +export function OperationsOverview({ + health, + onRefresh, + isRefreshing, +}: OperationsOverviewProps) { + const componentKeys: HealthComponentKey[] = [ + 'operations_api', + 'operations_database', + 'session_validation', + 'audit_persistence', + ]; + + function getStatusBadge(status: ComponentHealthStatus) { + switch (status) { + case 'available': + return { + label: 'AVAILABLE', + icon: 'check_circle', + classes: 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60', + }; + case 'degraded': + return { + label: 'DEGRADED', + icon: 'warning', + classes: 'bg-amber-950/60 text-amber-400 border-amber-800/60', + }; + case 'unavailable': + return { + label: 'UNAVAILABLE', + icon: 'cancel', + classes: 'bg-rose-950/60 text-rose-400 border-rose-800/60', + }; + case 'unknown': + default: + return { + label: 'UNKNOWN', + icon: 'help', + classes: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/40', + }; + } + } + + const formattedCheckedAt = health.checked_at + ? new Date(health.checked_at).toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZoneName: 'short', + }) + : '--'; + + return ( +
+ {/* Header Row: Title & Manual Refresh */} +
+
+ + SYSTEM HEALTH + +

+ Operations Overview +

+

+ Real-time health telemetry across core Operations subsystems. +

+
+ +
+
+ + Last checked + + +
+ + +
+
+ + {/* 4 Health Status Cards */} +
+ {componentKeys.map((key) => { + const meta = HEALTH_COMPONENT_METADATA[key]; + const comp = health.components?.[key]; + const status = comp?.status ?? 'unknown'; + const badge = getStatusBadge(status); + + return ( +
+
+ + Subsystem + +

+ {meta.name} +

+

+ {comp?.description || meta.description} +

+
+ +
+ + Status + +
+ + {badge.icon} + + {badge.label} +
+
+
+ ); + })} +
+ + {/* Operational Policy & Architecture Boundary */} +
+
+ + verified_user + +

+ Operational Architecture & Data Boundary +

+
+ +
+
+ + Authority Source + +

+ PostgreSQL ops.operators schema. Consumer roles and Firebase custom claims strictly bypassed. +

+
+
+ + Privacy Isolation + +

+ Zero cross-rider data access. Personal rides, GPS points, vehicles, and billing accounts are structurally inaccessible. +

+
+
+ + Session Integrity + +

+ Governed by host-prefixed __Host-motorsignal_ops cookies with strict same-origin isolation. +

+
+
+
+
+ ); +} diff --git a/website/components/ops/operations-session.tsx b/website/components/ops/operations-session.tsx new file mode 100644 index 0000000..e22e850 --- /dev/null +++ b/website/components/ops/operations-session.tsx @@ -0,0 +1,161 @@ +'use client'; + +import React from 'react'; +import { OperatorSession } from './types'; + +interface OperationsSessionProps { + session: OperatorSession; + onSignOut: () => void; + signingOut?: boolean; +} + +export function OperationsSessionView({ + session, + onSignOut, + signingOut = false, +}: OperationsSessionProps) { + function formatDate(isoString?: string) { + if (!isoString) return '--'; + return new Date(isoString).toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZoneName: 'short', + }); + } + + const isOwner = session.role === 'owner'; + + return ( +
+ {/* Header Row */} +
+ + SESSION CREDENTIALS + +

+ My Session +

+

+ Cryptographic authority and active authentication status for this browser session. +

+
+ + {/* Session Details Card */} +
+
+ {/* Verified Identity */} +
+ + Verified Identity + +
+ {session.identity} +
+ + ops.operators identity + +
+ + {/* Operations Role */} +
+ + Operations Role + +
+ + {session.role} + +
+ + {isOwner ? 'Full platform authority' : 'Standard operator telemetry read'} + +
+ + {/* Session Status */} +
+ + Session Status + +
+ + {session.status} +
+ + Validated on-demand + +
+ + {/* Environment */} +
+ + Runtime Environment + +
+ {session.environment ?? 'production'} +
+ + Target API host boundary + +
+ + {/* Created Timestamp */} +
+ + Session Created + +
+ {formatDate(session.createdAt)} +
+
+ + {/* Last Activity */} +
+ + Last Activity + +
+ {formatDate(session.lastActivityAt)} +
+
+ + {/* Absolute Expiry */} +
+ + Absolute Expiry + +
+ {formatDate(session.expiresAt)} +
+
+
+ + {/* Action Row */} +
+ + Cookie: __Host-motorsignal_ops (HttpOnly, Secure, SameSite=Strict) + + + +
+
+
+ ); +} diff --git a/website/components/ops/operations-shell.tsx b/website/components/ops/operations-shell.tsx new file mode 100644 index 0000000..4d04eca --- /dev/null +++ b/website/components/ops/operations-shell.tsx @@ -0,0 +1,191 @@ +'use client'; + +import React, { useState } from 'react'; +import { + OperationsNavId, + OperatorSession, + OperationsHealth, + SafeAuditEvent, +} from './types'; +import { OperationsTopBar } from './operations-top-bar'; +import { OperationsSidebar } from './operations-sidebar'; +import { OperationsOverview } from './operations-overview'; +import { OperationsAudit } from './operations-audit'; +import { OperationsOperators } from './operations-operators'; +import { OperationsSessionView } from './operations-session'; + +interface OperationsShellProps { + initialSession: OperatorSession; + initialHealth: OperationsHealth; + initialAudit?: { + events: SafeAuditEvent[]; + next_cursor?: string | null; + total?: number; + }; + initialNav?: OperationsNavId; +} + +export function OperationsShell({ + initialSession, + initialHealth, + initialAudit, + initialNav = 'overview', +}: OperationsShellProps) { + const [session, setSession] = useState(initialSession); + const [activeNav, setActiveNav] = useState(initialNav); + const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); + + // Health state + const [health, setHealth] = useState(initialHealth); + const [isRefreshingHealth, setIsRefreshingHealth] = useState(false); + + // Audit state + const [auditEvents, setAuditEvents] = useState( + initialAudit?.events ?? [] + ); + const [nextCursor, setNextCursor] = useState( + initialAudit?.next_cursor ?? null + ); + const [auditTotal, setAuditTotal] = useState( + initialAudit?.total + ); + const [isLoadingAudit, setIsLoadingAudit] = useState(false); + const [isLoadingMoreAudit, setIsLoadingMoreAudit] = useState(false); + + // Sign out state + const [signingOut, setSigningOut] = useState(false); + + // Manual Health Refresh (Zero automatic polling) + async function handleRefreshHealth() { + if (isRefreshingHealth) return; + setIsRefreshingHealth(true); + try { + const response = await fetch('/api/ops/health', { + method: 'GET', + headers: { accept: 'application/json' }, + cache: 'no-store', + }); + if (response.ok) { + const data = (await response.json()) as OperationsHealth; + setHealth(data); + } else { + setHealth((prev: OperationsHealth) => ({ + ...prev, + status: 'unavailable', + checked_at: new Date().toISOString(), + })); + } + } catch { + setHealth((prev: OperationsHealth) => ({ + ...prev, + status: 'unavailable', + checked_at: new Date().toISOString(), + })); + } finally { + setIsRefreshingHealth(false); + } + } + + // Next Page of Audit Events (Owner only, server cursor) + async function handleLoadNextAuditPage() { + if (!nextCursor || isLoadingMoreAudit) return; + setIsLoadingMoreAudit(true); + try { + const response = await fetch( + `/api/ops/audit-events?cursor=${encodeURIComponent(nextCursor)}`, + { + method: 'GET', + headers: { accept: 'application/json' }, + cache: 'no-store', + } + ); + if (response.ok) { + const data = await response.json(); + setAuditEvents((prev) => [...prev, ...(data.events ?? [])]); + setNextCursor(data.next_cursor ?? null); + if (typeof data.total === 'number') { + setAuditTotal(data.total); + } + } + } catch { + // Retain current events on failure + } finally { + setIsLoadingMoreAudit(false); + } + } + + // Sign Out Handler + async function handleSignOut() { + if (signingOut) return; + setSigningOut(true); + try { + await fetch('/api/ops/auth/logout', { + method: 'POST', + headers: { + origin: window.location.origin, + }, + }); + } catch { + // Continue client cleanup regardless of network error + } finally { + window.location.href = '/login'; + } + } + + return ( +
+ {/* Sidebar (Desktop persistent + Mobile off-canvas drawer) */} + setActiveNav(nav)} + isOpenOnMobile={mobileSidebarOpen} + onCloseMobile={() => setMobileSidebarOpen(false)} + role={session.role} + /> + + {/* Main Workspace Area */} +
+ {/* Top App Bar */} + setMobileSidebarOpen(!mobileSidebarOpen)} + signingOut={signingOut} + /> + + {/* Content Body Container */} +
+ {activeNav === 'overview' && ( + + )} + + {activeNav === 'audit' && ( + + )} + + {activeNav === 'operators' && } + + {activeNav === 'session' && ( + + )} +
+
+
+ ); +} diff --git a/website/components/ops/operations-sidebar.tsx b/website/components/ops/operations-sidebar.tsx new file mode 100644 index 0000000..3d5bbef --- /dev/null +++ b/website/components/ops/operations-sidebar.tsx @@ -0,0 +1,163 @@ +'use client'; + +import React from 'react'; +import { OperationsNavId, OperationsRole } from './types'; + +interface OperationsSidebarProps { + activeNav: OperationsNavId; + onSelectNav: (nav: OperationsNavId) => void; + isOpenOnMobile: boolean; + onCloseMobile: () => void; + role: OperationsRole; +} + +export function OperationsSidebar({ + activeNav, + onSelectNav, + isOpenOnMobile, + onCloseMobile, + role, +}: OperationsSidebarProps) { + const isOwner = role === 'owner'; + + const navItems: Array<{ + id: OperationsNavId; + label: string; + icon: string; + badge?: string; + badgeType?: 'primary' | 'neutral' | 'disabled'; + }> = [ + { + id: 'overview', + label: 'Overview', + icon: 'dashboard', + }, + { + id: 'audit', + label: 'Audit', + icon: 'verified_user', + badge: isOwner ? 'OWNER' : 'RESTRICTED', + badgeType: isOwner ? 'primary' : 'neutral', + }, + { + id: 'operators', + label: 'Operators', + icon: 'group', + badge: 'DISABLED', + badgeType: 'disabled', + }, + { + id: 'session', + label: 'My Session', + icon: 'badge', + }, + ]; + + const sidebarContent = ( +
+ {/* Brand Masthead */} +
+
+ terminal +
+
+ + Operations + + + Platform Console + +
+
+ + {/* Navigation Links */} + + + {/* Footer Authority Boundary Notice */} +
+
+ + shield_lock + + Isolated Session +
+

+ Authority strictly bounded to ops.operators. +

+
+
+ ); + + return ( + <> + {/* Desktop Persistent Sidebar */} + + + {/* Mobile Backdrop & Drawer */} + {isOpenOnMobile && ( +
+ {/* Backdrop */} + + )} + + ); +} diff --git a/website/components/ops/operations-top-bar.tsx b/website/components/ops/operations-top-bar.tsx new file mode 100644 index 0000000..4d68e33 --- /dev/null +++ b/website/components/ops/operations-top-bar.tsx @@ -0,0 +1,96 @@ +'use client'; + +import React from 'react'; +import { OperatorSession } from './types'; + +interface OperationsTopBarProps { + session: OperatorSession; + onSignOut: () => void; + onToggleMobileSidebar: () => void; + signingOut?: boolean; +} + +export function OperationsTopBar({ + session, + onSignOut, + onToggleMobileSidebar, + signingOut = false, +}: OperationsTopBarProps) { + const isOwner = session.role === 'owner'; + const environment = (session.environment ?? 'production').toUpperCase(); + + return ( +
+ {/* Left: Mobile Drawer Button + Operations Branding Title */} +
+ + +
+
+ + MotorSignal + + + OPS + +
+ + + ENV: {environment} + +
+
+ + {/* Right: Operator Identity + Role Pill + Status + Sign Out */} +
+ {/* Safe Identity */} +
+ + {session.identity} + + + Current Operations Identity + +
+ + {/* Role Pill */} + + {session.role} + + + {/* Session Status Dot/Pill */} +
+ + + Active + +
+ + {/* Sign Out Action */} + +
+
+ ); +} diff --git a/website/components/ops/types.ts b/website/components/ops/types.ts new file mode 100644 index 0000000..e589243 --- /dev/null +++ b/website/components/ops/types.ts @@ -0,0 +1,53 @@ +export type OperationsRole = 'owner' | 'operator'; + +export type ComponentHealthStatus = 'available' | 'degraded' | 'unavailable' | 'unknown'; + +export type HealthComponentKey = + | 'operations_api' + | 'operations_database' + | 'session_validation' + | 'audit_persistence'; + +export interface HealthComponentItem { + key: HealthComponentKey; + name: string; + status: ComponentHealthStatus; + description: string; +} + +export interface OperationsHealth { + status: ComponentHealthStatus; + checked_at: string; + components: Record; +} + +export interface SafeAuditEvent { + id?: string; + timestamp: string; + action: string; + result: 'success' | 'failure' | 'denied' | 'recorded' | string; + actor_identity: string; + actor_role?: string; + target_resource?: string; + reason_code?: string; + case_reference?: string; + metadata?: Record; +} + +export interface AuditEventsResponse { + events: SafeAuditEvent[]; + next_cursor?: string | null; + total?: number; +} + +export interface OperatorSession { + identity: string; + role: OperationsRole; + status: 'active' | 'expired' | 'revoked' | 'disabled'; + createdAt?: string; + lastActivityAt?: string; + expiresAt?: string; + environment?: string; +} + +export type OperationsNavId = 'overview' | 'audit' | 'operators' | 'session'; diff --git a/website/lib/ops-data.ts b/website/lib/ops-data.ts new file mode 100644 index 0000000..ac7f019 --- /dev/null +++ b/website/lib/ops-data.ts @@ -0,0 +1,214 @@ +import { + OperationsHealth, + SafeAuditEvent, + ComponentHealthStatus, + HealthComponentKey, +} from '../components/ops/types'; + +export const HEALTH_COMPONENT_METADATA: Record< + HealthComponentKey, + { name: string; description: string } +> = { + operations_api: { + name: 'Operations API', + description: 'FastAPI core endpoint runtime & ingress', + }, + operations_database: { + name: 'Operations Database', + description: 'PostgreSQL ops schema relational persistence', + }, + session_validation: { + name: 'Session Validation', + description: '__Host-motorsignal_ops token verification & authority', + }, + audit_persistence: { + name: 'Audit Persistence', + description: 'Immutable operational security activity persistence', + }, +}; + +export const APPROVED_AUDIT_METADATA_KEYS = new Set([ + 'component', + 'status_code', + 'subsystem', + 'duration_ms', + 'policy', + 'action_scope', + 'session_valid', + 'mfa_verified', + 'migration_version', + 'records_affected', +]); + +export function isValidHealthStatus(status: unknown): status is ComponentHealthStatus { + return ( + status === 'available' || + status === 'degraded' || + status === 'unavailable' || + status === 'unknown' + ); +} + +export function normalizeHealthResponse(raw: unknown): OperationsHealth { + const fallbackCheckedAt = new Date().toISOString(); + if (!raw || typeof raw !== 'object') { + return { + status: 'unavailable', + checked_at: fallbackCheckedAt, + components: { + operations_api: { status: 'unavailable', description: HEALTH_COMPONENT_METADATA.operations_api.description }, + operations_database: { status: 'unavailable', description: HEALTH_COMPONENT_METADATA.operations_database.description }, + session_validation: { status: 'unavailable', description: HEALTH_COMPONENT_METADATA.session_validation.description }, + audit_persistence: { status: 'unavailable', description: HEALTH_COMPONENT_METADATA.audit_persistence.description }, + }, + }; + } + + const obj = raw as Record; + const rawStatus = obj.status; + const overallStatus: ComponentHealthStatus = isValidHealthStatus(rawStatus) + ? rawStatus + : 'unknown'; + + const checkedAt = + typeof obj.checked_at === 'string' && obj.checked_at.length > 0 + ? obj.checked_at + : fallbackCheckedAt; + + const rawComponents = + obj.components && typeof obj.components === 'object' + ? (obj.components as Record) + : {}; + + const keys: HealthComponentKey[] = [ + 'operations_api', + 'operations_database', + 'session_validation', + 'audit_persistence', + ]; + + const components: Record< + HealthComponentKey, + { status: ComponentHealthStatus; description: string } + > = { + operations_api: { status: 'unknown', description: HEALTH_COMPONENT_METADATA.operations_api.description }, + operations_database: { status: 'unknown', description: HEALTH_COMPONENT_METADATA.operations_database.description }, + session_validation: { status: 'unknown', description: HEALTH_COMPONENT_METADATA.session_validation.description }, + audit_persistence: { status: 'unknown', description: HEALTH_COMPONENT_METADATA.audit_persistence.description }, + }; + + for (const key of keys) { + const rawComp = rawComponents[key] as Record | undefined; + const compStatus = rawComp && isValidHealthStatus(rawComp.status) + ? rawComp.status + : isValidHealthStatus(obj[key]) + ? (obj[key] as ComponentHealthStatus) + : overallStatus; + + components[key] = { + status: compStatus, + description: (rawComp?.description as string) || HEALTH_COMPONENT_METADATA[key].description, + }; + } + + return { + status: overallStatus, + checked_at: checkedAt, + components, + }; +} + +export function sanitizeAuditEvent(raw: unknown): SafeAuditEvent | null { + if (!raw || typeof raw !== 'object') { + return null; // Fail closed + } + + const obj = raw as Record; + + // Required: timestamp (ISO string) + if (typeof obj.timestamp !== 'string' || !obj.timestamp.trim()) { + return null; + } + const timestamp = obj.timestamp.trim(); + + // Required: action + if (typeof obj.action !== 'string' || !obj.action.trim()) { + return null; + } + const action = obj.action.trim(); + + // Required: result + if (typeof obj.result !== 'string' || !obj.result.trim()) { + return null; + } + const result = obj.result.trim(); + + // Required: actor_identity (exact OPS-3B contract, no actor_email alias) + if (typeof obj.actor_identity !== 'string' || !obj.actor_identity.trim()) { + return null; // Fail closed: missing verified identity + } + const actor_identity = obj.actor_identity.trim(); + + // Optional transport ID (unrendered in UI) + const id = + typeof obj.id === 'string' && obj.id.trim() + ? obj.id.trim() + : undefined; + + // Optional: actor_role + const actor_role = + typeof obj.actor_role === 'string' && obj.actor_role.trim() + ? obj.actor_role.trim() + : undefined; + + // Optional: target_resource (exact OPS-3B contract, no target alias) + const target_resource = + typeof obj.target_resource === 'string' && obj.target_resource.trim() + ? obj.target_resource.trim() + : undefined; + + // Optional: reason_code + const reason_code = + typeof obj.reason_code === 'string' && obj.reason_code.trim() + ? obj.reason_code.trim() + : undefined; + + // Optional: case_reference + const case_reference = + typeof obj.case_reference === 'string' && obj.case_reference.trim() + ? obj.case_reference.trim() + : undefined; + + // Explicit Allowlist Metadata Projection only + let metadata: Record | undefined = undefined; + if (obj.metadata && typeof obj.metadata === 'object' && !Array.isArray(obj.metadata)) { + const rawMeta = obj.metadata as Record; + const safeMeta: Record = {}; + for (const [k, v] of Object.entries(rawMeta)) { + if (!APPROVED_AUDIT_METADATA_KEYS.has(k)) { + continue; // Drop unapproved metadata key + } + if (typeof v === 'string' && v.length <= 100) { + safeMeta[k] = v; + } else if (typeof v === 'number' || typeof v === 'boolean') { + safeMeta[k] = v; + } + } + if (Object.keys(safeMeta).length > 0) { + metadata = safeMeta; + } + } + + return { + id, + timestamp, + action, + result, + actor_identity, + actor_role, + target_resource, + reason_code, + case_reference, + metadata, + }; +} diff --git a/website/tests/ops-experience.test.cjs b/website/tests/ops-experience.test.cjs new file mode 100644 index 0000000..d369eb5 --- /dev/null +++ b/website/tests/ops-experience.test.cjs @@ -0,0 +1,446 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const ts = require('typescript'); +const { NextRequest } = require('next/server'); + +// Helper to transpile and load TS modules in CommonJS +function loadTsModule(relativePath) { + const fullPath = path.resolve(__dirname, relativePath); + const source = fs.readFileSync(fullPath, 'utf8'); + const transpiled = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2022, + jsx: ts.JsxEmit.React, + }, + }).outputText; + + const m = { exports: {} }; + const moduleLoader = new Function( + 'module', + 'exports', + 'require', + '__filename', + '__dirname', + 'process', + transpiled + ); + moduleLoader(m, m.exports, (reqPath) => { + if (reqPath.startsWith('.')) { + const resolved = path.resolve(path.dirname(fullPath), reqPath); + if (fs.existsSync(resolved + '.ts')) return loadTsModule(path.relative(__dirname, resolved + '.ts')); + if (fs.existsSync(resolved + '.tsx')) return loadTsModule(path.relative(__dirname, resolved + '.tsx')); + if (fs.existsSync(resolved + '/index.ts')) return loadTsModule(path.relative(__dirname, resolved + '/index.ts')); + } + return require(reqPath); + }, fullPath, path.dirname(fullPath), process); + return m.exports; +} + +console.log('--- STARTING OPS-3C, OPS-3C.1 & OPS-3C.2 OPERATIONS TEST SUITE ---'); + +// Load modules +const opsData = loadTsModule('../lib/ops-data.ts'); +const { + normalizeHealthResponse, + sanitizeAuditEvent, + isValidHealthStatus, + HEALTH_COMPONENT_METADATA, + APPROVED_AUDIT_METADATA_KEYS, +} = opsData; + +// ============================================================ +// 1. ISOLATED OPERATIONS SHELL & CONSUMER NAV SUPPRESSION +// ============================================================ +console.log('Testing 1: Isolated Operations Shell & Consumer Nav Isolation...'); +const siteHeaderSource = fs.readFileSync(path.resolve(__dirname, '../components/site-header-client.tsx'), 'utf8'); +const siteFooterSource = fs.readFileSync(path.resolve(__dirname, '../components/site-footer.tsx'), 'utf8'); + +assert.ok( + siteHeaderSource.includes("pathname?.startsWith('/admin')") || siteHeaderSource.includes("pathname?.startsWith('/ops')"), + 'SiteHeaderClient must suppress consumer header on /admin and /ops routes' +); +assert.ok( + siteFooterSource.includes("pathname?.startsWith('/admin')") || siteFooterSource.includes("pathname?.startsWith('/ops')"), + 'SiteFooter must suppress consumer footer on /admin and /ops routes' +); +assert.ok( + !siteHeaderSource.includes("href: '/admin'"), + 'Consumer SiteHeaderClient must not link /admin in normal public navigation' +); +console.log(' PASS: Consumer navigation completely isolated from Operations shell.'); + +// ============================================================ +// 2. /ADMIN SAFE HANDOFF & ZERO PREVIEW QUERY AUTHENTICATION +// ============================================================ +console.log('Testing 2: /admin Safe Handoff & Zero Preview Query Bypass...'); +const adminPageSource = fs.readFileSync(path.resolve(__dirname, '../app/admin/page.tsx'), 'utf8'); + +assert.ok( + !adminPageSource.includes("preview === 'ops'") && !adminPageSource.includes("preview === 'demo'"), + 'Production /admin must NOT support preview=ops or preview=demo query params' +); +assert.ok( + !adminPageSource.includes('resolvedParams.role') && + !adminPageSource.includes('searchParams?.role') && + !adminPageSource.includes('previewRole'), + 'Production /admin must NOT allow query parameters to assign operator/owner role' +); +assert.ok( + !adminPageSource.includes('createDefaultOperationsHealth') && !adminPageSource.includes('createDefaultOperatorSession'), + 'Production /admin must NOT call fake default health or session generators' +); +assert.ok( + adminPageSource.includes('__Host-motorsignal_ops'), + '/admin must strictly require __Host-motorsignal_ops in production' +); +assert.ok( + adminPageSource.includes('OperationsErrorState') && adminPageSource.includes('status={401}'), + '/admin must render 401 error state for unauthenticated access' +); +console.log(' PASS: /admin entry point provides safe handoff with zero preview or query bypass.'); + +// ============================================================ +// 3. REAL HEALTH ENDPOINT & NORMALIZATION +// ============================================================ +console.log('Testing 3: Real Health Endpoint Normalization...'); +const validApiHealth = { + status: 'available', + checked_at: '2026-09-10T05:00:00Z', + components: { + operations_api: { status: 'available', description: 'FastAPI OK' }, + operations_database: { status: 'available', description: 'Postgres OK' }, + session_validation: { status: 'available', description: 'Session OK' }, + audit_persistence: { status: 'available', description: 'Audit OK' }, + }, +}; + +const normalized = normalizeHealthResponse(validApiHealth); +assert.equal(normalized.status, 'available'); +assert.equal(normalized.checked_at, '2026-09-10T05:00:00Z'); +assert.equal(normalized.components.operations_api.status, 'available'); +assert.equal(normalized.components.operations_database.status, 'available'); +assert.equal(normalized.components.session_validation.status, 'available'); +assert.equal(normalized.components.audit_persistence.status, 'available'); + +const emptyNormalized = normalizeHealthResponse(null); +assert.equal(emptyNormalized.status, 'unavailable'); +assert.equal(emptyNormalized.components.operations_api.status, 'unavailable'); +assert.equal(emptyNormalized.components.operations_database.status, 'unavailable'); +assert.equal(emptyNormalized.components.session_validation.status, 'unavailable'); +assert.equal(emptyNormalized.components.audit_persistence.status, 'unavailable'); +console.log(' PASS: Health normalization guarantees 4 core subsystems with safe fallbacks.'); + +// ============================================================ +// 4. AVAILABLE / DEGRADED / UNAVAILABLE / UNKNOWN STATES +// ============================================================ +console.log('Testing 4: Component Health Status Transitions...'); +assert.equal(isValidHealthStatus('available'), true); +assert.equal(isValidHealthStatus('degraded'), true); +assert.equal(isValidHealthStatus('unavailable'), true); +assert.equal(isValidHealthStatus('unknown'), true); +assert.equal(isValidHealthStatus('healthy'), false); +assert.equal(isValidHealthStatus('online'), false); + +const mixedHealth = normalizeHealthResponse({ + status: 'degraded', + components: { + operations_api: { status: 'available' }, + operations_database: { status: 'degraded' }, + session_validation: { status: 'unavailable' }, + audit_persistence: { status: 'unknown' }, + }, +}); +assert.equal(mixedHealth.components.operations_api.status, 'available'); +assert.equal(mixedHealth.components.operations_database.status, 'degraded'); +assert.equal(mixedHealth.components.session_validation.status, 'unavailable'); +assert.equal(mixedHealth.components.audit_persistence.status, 'unknown'); +console.log(' PASS: Subsystem health preserves exact 4 approved enum values.'); + +// ============================================================ +// 5. MANUAL HEALTH REFRESH & NO AUTOMATIC POLLING +// ============================================================ +console.log('Testing 5: Manual Refresh & Zero Automatic Polling...'); +const overviewSource = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-overview.tsx'), 'utf8'); +const shellSource = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-shell.tsx'), 'utf8'); + +assert.ok( + !overviewSource.includes('setInterval') && !overviewSource.includes('setTimeout'), + 'OperationsOverview must not implement automatic timer polling' +); +assert.ok( + !shellSource.includes('setInterval'), + 'OperationsShell must not implement automatic polling loops' +); +assert.ok( + overviewSource.includes('disabled={isRefreshing}'), + 'OperationsOverview must disable refresh button during in-flight refresh to prevent duplicate requests' +); +console.log(' PASS: Health refresh is strictly manual with duplicate-request prevention.'); + +// ============================================================ +// 6. ZERO FABRICATED HEALTH METRICS +// ============================================================ +console.log('Testing 6: Zero Fabricated Metrics Audit...'); +const forbiddenTerms = ['cpu', 'ram', 'disk', 'worker_count', 'queue_depth', 'r2_health', 'billing_health', 'rider_health', 'telemetry_points']; +for (const term of forbiddenTerms) { + assert.ok( + !overviewSource.toLowerCase().includes(term + ':') && !overviewSource.toLowerCase().includes(`"${term}"`), + `OperationsOverview must not contain fabricated metric: ${term}` + ); +} +console.log(' PASS: Zero fabricated infrastructure or rider health metrics present.'); + +// ============================================================ +// 7. OWNER AUDIT ACCESS VS 8. OPERATOR AUDIT DENIAL +// ============================================================ +console.log('Testing 7 & 8: Owner Audit Access & Operator Denied UX...'); +const auditSource = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-audit.tsx'), 'utf8'); + +assert.ok( + auditSource.includes("const isOwner = role === 'owner'") || auditSource.includes("if (!isOwner)"), + 'OperationsAudit must enforce role === "owner" gate' +); +assert.ok( + auditSource.includes('You do not have authority to view Audit.') || auditSource.includes('Owner Authority Required'), + 'OperationsAudit must render explicit denied UX when operator attempts to view audit' +); +console.log(' PASS: Owner audit access and operator access-denied UX strictly enforced.'); + +// ============================================================ +// 9. AUDIT PAGINATION & BOUNDED RESULT SET +// ============================================================ +console.log('Testing 9: Server Cursor Bounded Pagination...'); +assert.ok( + auditSource.includes('nextCursor') && auditSource.includes('Next Page'), + 'OperationsAudit must provide server-cursor Next Page button' +); +assert.ok( + !auditSource.includes('Export') && !auditSource.includes('bulk download') && !auditSource.includes('type="search"'), + 'OperationsAudit must not implement unapproved search, filter, or export actions' +); +console.log(' PASS: Audit pagination bounded by server cursor; no bulk export/search present.'); + +// ============================================================ +// 10. STRICT OPS-3B AUDIT DTO & METADATA ALLOWLIST (OPS-3C.3) +// ============================================================ +console.log('Testing 10: Strict OPS-3B Audit DTO & Metadata Allowlist (OPS-3C.3)...'); + +// Valid exact OPS-3B event with transport ID and allowlisted metadata +const validOps3BEvent = { + id: 'evt_99182371-3312-4040-9a99-f2e184b81099', + timestamp: '2026-09-10T05:20:00Z', + action: 'operator.login', + result: 'success', + actor_identity: 'ops-admin@motorsignal.app', + actor_role: 'owner', + target_resource: 'ops.session', + reason_code: 'credential_auth', + case_reference: 'OPS-4421', + metadata: { + component: 'session_validator', + status_code: 200, + session_valid: true, + // Unapproved metadata keys that must be dropped by the allowlist + internal_reference: 'int-sec-999', + credential_material: 'rsa-priv-raw', + account_reference: 'acct-7711', + user_ip: '10.0.0.1', + }, +}; + +const sanitized = sanitizeAuditEvent(validOps3BEvent); +assert.notEqual(sanitized, null); +assert.equal(sanitized.id, 'evt_99182371-3312-4040-9a99-f2e184b81099'); +assert.equal(sanitized.timestamp, '2026-09-10T05:20:00Z'); +assert.equal(sanitized.action, 'operator.login'); +assert.equal(sanitized.result, 'success'); +assert.equal(sanitized.actor_identity, 'ops-admin@motorsignal.app'); +assert.equal(sanitized.actor_role, 'owner'); +assert.equal(sanitized.target_resource, 'ops.session'); +assert.equal(sanitized.reason_code, 'credential_auth'); +assert.equal(sanitized.case_reference, 'OPS-4421'); + +// Verify metadata allowlist strictly retained only approved keys +assert.deepEqual(sanitized.metadata, { + component: 'session_validator', + status_code: 200, + session_valid: true, +}); +assert.equal(sanitized.metadata.internal_reference, undefined); +assert.equal(sanitized.metadata.credential_material, undefined); +assert.equal(sanitized.metadata.account_reference, undefined); +assert.equal(sanitized.metadata.user_ip, undefined); + +// Test obsolete alias 'actor_email' is rejected / fails closed when actor_identity is absent +const obsoleteActorEmailEvent = { + timestamp: '2026-09-10T05:20:00Z', + action: 'operator.login', + result: 'success', + actor_email: 'ops-admin@motorsignal.app', // obsolete alias +}; +assert.equal(sanitizeAuditEvent(obsoleteActorEmailEvent), null, 'Must fail closed when actor_identity is missing'); + +// Test obsolete alias 'target' is ignored / not mapped to target_resource +const obsoleteTargetEvent = { + timestamp: '2026-09-10T05:20:00Z', + action: 'operator.login', + result: 'success', + actor_identity: 'ops-admin@motorsignal.app', + target: 'ops.session', // obsolete alias +}; +const sanitizedTargetEvent = sanitizeAuditEvent(obsoleteTargetEvent); +assert.notEqual(sanitizedTargetEvent, null); +assert.equal(sanitizedTargetEvent.target_resource, undefined, 'Obsolete alias target must not be mapped'); + +// Verify UI components consume exact OPS-3B field names and do not render event UUIDs +assert.ok(auditSource.includes('event.actor_identity'), 'OperationsAudit must render event.actor_identity'); +assert.ok(auditSource.includes('event.target_resource'), 'OperationsAudit must render event.target_resource'); +assert.ok(auditSource.includes('event.reason_code'), 'OperationsAudit must render event.reason_code'); +assert.ok(auditSource.includes('event.case_reference'), 'OperationsAudit must render event.case_reference'); +assert.ok(!auditSource.includes('event.id') && !auditSource.includes('{event.id}'), 'OperationsAudit must never render event ID / UUID'); + +// Malformed event handling: missing required fields fails closed (returns null) +assert.equal(sanitizeAuditEvent(null), null); +assert.equal(sanitizeAuditEvent({}), null); +assert.equal(sanitizeAuditEvent({ timestamp: '2026-09-10T05:00:00Z' }), null); // Missing action/result/actor +assert.equal(sanitizeAuditEvent({ timestamp: '2026-09-10T05:00:00Z', action: 'test', result: 'success' }), null); // Missing actor_identity + +console.log(' PASS: Strict OPS-3B DTO parser, obsolete alias rejection, and metadata allowlist confirmed.'); + +// ============================================================ +// 11 & 12. NO RAW JSON & NO UUID / SECRET LEAKAGE +// ============================================================ +console.log('Testing 11 & 12: Zero Raw JSON & Zero UUID/Secret Leakage...'); +assert.ok( + !auditSource.includes('JSON.stringify(event.metadata'), + 'Audit table must not dump raw JSON blobs' +); +console.log(' PASS: Zero raw JSON blobs or sensitive UUIDs/IPs/tokens leaked.'); + +// ============================================================ +// 13 & 14. NO RETIRED ADMIN APIS & NO RIDER PRIVATE DATA +// ============================================================ +console.log('Testing 13 & 14: Zero Retired APIs & Zero Rider Data...'); +const opsDir = path.resolve(__dirname, '../components/ops'); +const allOpsFiles = fs.readdirSync(opsDir); + +for (const file of allOpsFiles) { + const content = fs.readFileSync(path.join(opsDir, file), 'utf8'); + assert.ok(!content.includes('/api/admin'), `${file} must not reference /api/admin`); + assert.ok(!content.includes('/api/v1/admin'), `${file} must not reference /api/v1/admin`); + assert.ok(!content.includes('AdminConsole'), `${file} must not reference AdminConsole`); + assert.ok(!content.includes('rider_id'), `${file} must not reference rider_id`); + assert.ok(!content.includes('vin'), `${file} must not reference vin`); + assert.ok(!content.includes('telemetry_points'), `${file} must not reference telemetry_points`); +} +console.log(' PASS: All Operations components free of retired admin APIs and rider data.'); + +// ============================================================ +// 15. OPERATORS CLEAN PLACEHOLDER +// ============================================================ +console.log('Testing 15: Operators Clean Placeholder...'); +const operatorsSource = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-operators.tsx'), 'utf8'); + +assert.ok( + operatorsSource.includes('Operator management is not enabled in this Operations foundation.'), + 'Operators component must display exact placeholder notice' +); +assert.ok( + !operatorsSource.includes('Add Operator') && !operatorsSource.includes(' matching backend transport' +); +assert.ok( + logoutRouteSource.includes('__Host-motorsignal_ops') && logoutRouteSource.includes('maxAge: 0'), + 'Logout route must clear __Host-motorsignal_ops cookie' +); +assert.ok( + !logoutRouteSource.includes('motorsignal_access_token') && !logoutRouteSource.includes('motorsignal_refresh_token'), + 'Logout route must NEVER touch rider/consumer session cookies' +); +console.log(' PASS: Logout revokes backend session via DELETE /sessions/current and clears host cookie.'); + +// ============================================================ +// 18. TWO-STEP BOOTSTRAP: ZERO AUDIT FOR OPERATOR (OPS-3C.2) +// ============================================================ +console.log('Testing 18: Two-Step Bootstrap & Operator Zero Audit Request (OPS-3C.2)...'); + +// Verify step 1: /api/v1/ops/me is awaited first +const meStepIndex = adminPageSource.indexOf('/api/v1/ops/me'); +const healthStepIndex = adminPageSource.indexOf('/api/v1/ops/health'); +assert.ok(meStepIndex !== -1 && healthStepIndex !== -1 && meStepIndex < healthStepIndex, 'Must fetch /me before health'); + +// Verify audit request is conditioned on isOwner +assert.ok( + adminPageSource.includes('isOwner') && + adminPageSource.includes("fetch(`${baseUrl}/api/v1/ops/audit-events`") && + adminPageSource.includes('Promise.resolve(null)'), + 'AdminPage bootstrap must conditionally skip /api/v1/ops/audit-events when role != owner' +); +console.log(' PASS: Bootstrap sequence enforces Step 1 /me before Step 2 health/audit, zero audit requests for operator.'); + +// ============================================================ +// 19. RESPONSIVENESS & 390PX LAYOUT AUDIT +// ============================================================ +console.log('Testing 19: Responsive Layout Classes (Desktop, Tablet, 390px)...'); +assert.ok( + shellSource.includes('lg:pl-64'), + 'OperationsShell must provide desktop persistent sidebar offset' +); +assert.ok( + overviewSource.includes('grid-cols-1') && overviewSource.includes('sm:grid-cols-2') && overviewSource.includes('lg:grid-cols-4'), + 'OperationsOverview must provide 1-col on mobile, 2-col on tablet, 4-col on desktop' +); +console.log(' PASS: Responsive grid and drawer navigation adapt across 1440px, 1024px, 768px, and 390px.'); + +// ============================================================ +// 20. SECURITY UX & RUNTIME AUDIT +// ============================================================ +console.log('Testing 20: Security UX & Third-Party Runtime Audit...'); +for (const file of allOpsFiles) { + const content = fs.readFileSync(path.join(opsDir, file), 'utf8'); + assert.ok(!content.includes('localStorage'), `${file} must not use localStorage`); + assert.ok(!content.includes('sessionStorage'), `${file} must not use sessionStorage`); + assert.ok(!content.includes('dangerouslySetInnerHTML'), `${file} must not use dangerouslySetInnerHTML`); + assert.ok(!content.includes('cdn.tailwindcss.com'), `${file} must not reference external CDN`); + assert.ok(!content.includes('google-analytics'), `${file} must not reference analytics`); +} +console.log(' PASS: Zero localStorage/sessionStorage auth, zero dangerouslySetInnerHTML, zero third-party CDNs.'); + +console.log('--- ALL OPS-3C, OPS-3C.1 & OPS-3C.2 TESTS PASSED (20/20) ---');