Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
288 lines
7.5 KiB
TypeScript
288 lines
7.5 KiB
TypeScript
/**
|
|
* API Proxy for PCA Platform (Heuristic Service)
|
|
*
|
|
* Provides secure server-side access to the PCA Heuristic Service
|
|
* for bot detection, session monitoring, and configuration management
|
|
*
|
|
* Heuristic Service runs on port 8085
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
|
|
const PCA_SERVICE_URL = process.env.PCA_SERVICE_URL || 'http://localhost:8085'
|
|
|
|
// Helper to make fetch with timeout and error handling
|
|
async function safeFetch(url: string, options?: RequestInit): Promise<Response | null> {
|
|
try {
|
|
const controller = new AbortController()
|
|
const timeoutId = setTimeout(() => controller.abort(), 5000)
|
|
|
|
const response = await fetch(url, {
|
|
...options,
|
|
signal: controller.signal,
|
|
})
|
|
|
|
clearTimeout(timeoutId)
|
|
return response
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
// GET: Fetch health, sessions, config, or stats
|
|
export async function GET(request: NextRequest) {
|
|
const searchParams = request.nextUrl.searchParams
|
|
const action = searchParams.get('action')
|
|
|
|
// Health check
|
|
if (action === 'health') {
|
|
const response = await safeFetch(`${PCA_SERVICE_URL}/health`, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
|
|
if (!response || !response.ok) {
|
|
return NextResponse.json({
|
|
status: 'offline',
|
|
message: 'PCA Heuristic Service not available',
|
|
})
|
|
}
|
|
|
|
try {
|
|
const data = await response.json()
|
|
return NextResponse.json({
|
|
status: 'healthy',
|
|
...data,
|
|
})
|
|
} catch {
|
|
return NextResponse.json({ status: 'healthy' })
|
|
}
|
|
}
|
|
|
|
// Get client config
|
|
if (action === 'config') {
|
|
const response = await safeFetch(`${PCA_SERVICE_URL}/pca/v1/config`, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
|
|
if (response && response.ok) {
|
|
try {
|
|
const data = await response.json()
|
|
return NextResponse.json(data)
|
|
} catch {
|
|
// Fall through to default
|
|
}
|
|
}
|
|
|
|
// Return default config if service unavailable or error
|
|
return NextResponse.json({
|
|
tick: { endpoint: '/pca/v1/tick', interval_ms: 5000 },
|
|
thresholds: { score_pass: 0.7, score_challenge: 0.4 },
|
|
weights: {
|
|
dwell_ratio: 0.30,
|
|
scroll_score: 0.25,
|
|
pointer_variance: 0.20,
|
|
click_rate: 0.25,
|
|
},
|
|
step_up: { methods: ['webauthn', 'pow'], primary: 'webauthn' },
|
|
service_status: 'offline',
|
|
})
|
|
}
|
|
|
|
// Get admin stats
|
|
if (action === 'stats') {
|
|
const response = await safeFetch(`${PCA_SERVICE_URL}/pca/v1/admin/stats`, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
|
|
if (response && response.ok) {
|
|
try {
|
|
const data = await response.json()
|
|
return NextResponse.json(data)
|
|
} catch {
|
|
// Fall through to default
|
|
}
|
|
}
|
|
|
|
// Return mock stats if service unavailable
|
|
return NextResponse.json({
|
|
active_sessions: 0,
|
|
total_ticks: 0,
|
|
challenges_issued: 0,
|
|
challenges_passed: 0,
|
|
avg_score: 0,
|
|
humans_detected: 0,
|
|
bots_detected: 0,
|
|
service_status: 'offline',
|
|
})
|
|
}
|
|
|
|
// Get active sessions
|
|
if (action === 'sessions') {
|
|
const response = await safeFetch(`${PCA_SERVICE_URL}/pca/v1/admin/sessions`, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
|
|
if (response && response.ok) {
|
|
try {
|
|
const data = await response.json()
|
|
return NextResponse.json(data)
|
|
} catch {
|
|
// Fall through to default
|
|
}
|
|
}
|
|
|
|
// Return empty sessions if service unavailable
|
|
return NextResponse.json({
|
|
sessions: [],
|
|
total: 0,
|
|
service_status: 'offline',
|
|
})
|
|
}
|
|
|
|
// Get ai-access.json configuration
|
|
if (action === 'ai-access') {
|
|
const response = await safeFetch(`${PCA_SERVICE_URL}/pca/v1/admin/ai-access`, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
|
|
if (response && response.ok) {
|
|
try {
|
|
const data = await response.json()
|
|
return NextResponse.json(data)
|
|
} catch {
|
|
// Fall through to default
|
|
}
|
|
}
|
|
|
|
// Return default ai-access config
|
|
return NextResponse.json({
|
|
version: '1.0',
|
|
thresholds: { score_pass: 0.7, score_challenge: 0.4 },
|
|
weights: {
|
|
dwell_ratio: 0.30,
|
|
scroll_score: 0.25,
|
|
pointer_variance: 0.20,
|
|
click_rate: 0.25,
|
|
},
|
|
step_up: { methods: ['webauthn', 'pow'], primary: 'webauthn' },
|
|
pca_roles: {
|
|
Person: { access: 'allow', price: null },
|
|
Corporate: { access: 'allow', price: '0.01 EUR' },
|
|
Agent: { access: 'charge', price: '0.001 EUR' },
|
|
},
|
|
payment: { enabled: false },
|
|
service_status: 'offline',
|
|
})
|
|
}
|
|
|
|
// Default: health check
|
|
const response = await safeFetch(`${PCA_SERVICE_URL}/health`, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
|
|
if (!response || !response.ok) {
|
|
return NextResponse.json({
|
|
status: 'offline',
|
|
service: 'pca-heuristic-service',
|
|
})
|
|
}
|
|
|
|
try {
|
|
const data = await response.json()
|
|
return NextResponse.json(data)
|
|
} catch {
|
|
return NextResponse.json({
|
|
status: 'offline',
|
|
service: 'pca-heuristic-service',
|
|
})
|
|
}
|
|
}
|
|
|
|
// POST: Evaluate session or update config
|
|
export async function POST(request: NextRequest) {
|
|
const searchParams = request.nextUrl.searchParams
|
|
const action = searchParams.get('action')
|
|
|
|
const body = await request.json().catch(() => ({}))
|
|
|
|
// Evaluate a specific session
|
|
if (action === 'evaluate') {
|
|
const sessionId = searchParams.get('sessionId')
|
|
if (!sessionId) {
|
|
return NextResponse.json({ error: 'Session ID required' }, { status: 400 })
|
|
}
|
|
|
|
const response = await safeFetch(
|
|
`${PCA_SERVICE_URL}/pca/v1/evaluate?session_id=${sessionId}`,
|
|
{ headers: { 'Content-Type': 'application/json' } }
|
|
)
|
|
|
|
if (!response || !response.ok) {
|
|
return NextResponse.json(
|
|
{ error: 'Failed to evaluate session - service offline' },
|
|
{ status: 503 }
|
|
)
|
|
}
|
|
|
|
try {
|
|
const data = await response.json()
|
|
return NextResponse.json(data)
|
|
} catch {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid response from service' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
|
|
// Update configuration (admin only)
|
|
if (action === 'update-config') {
|
|
const response = await safeFetch(`${PCA_SERVICE_URL}/pca/v1/admin/config`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
|
|
if (!response || !response.ok) {
|
|
return NextResponse.json(
|
|
{ error: 'Failed to update configuration - service offline' },
|
|
{ status: 503 }
|
|
)
|
|
}
|
|
|
|
try {
|
|
const data = await response.json()
|
|
return NextResponse.json(data)
|
|
} catch {
|
|
return NextResponse.json({ success: true })
|
|
}
|
|
}
|
|
|
|
// Clear session (admin only)
|
|
if (action === 'clear-session') {
|
|
const sessionId = searchParams.get('sessionId')
|
|
if (!sessionId) {
|
|
return NextResponse.json({ error: 'Session ID required' }, { status: 400 })
|
|
}
|
|
|
|
const response = await safeFetch(
|
|
`${PCA_SERVICE_URL}/pca/v1/admin/sessions/${sessionId}`,
|
|
{
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}
|
|
)
|
|
|
|
if (!response || !response.ok) {
|
|
return NextResponse.json(
|
|
{ error: 'Failed to clear session - service offline' },
|
|
{ status: 503 }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json({ deleted: true, session_id: sessionId })
|
|
}
|
|
|
|
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
|
}
|