fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
287
website/app/api/admin/pca/route.ts
Normal file
287
website/app/api/admin/pca/route.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 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 })
|
||||
}
|
||||
Reference in New Issue
Block a user