feat: BreakPilot PWA - Full codebase (clean push without large binaries)
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
This commit is contained in:
275
admin-v2/app/api/admin/agents/[agentId]/route.ts
Normal file
275
admin-v2/app/api/admin/agents/[agentId]/route.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
|
||||
/**
|
||||
* Individual Agent API
|
||||
*
|
||||
* GET - Get agent details including SOUL content
|
||||
* PUT - Update agent configuration
|
||||
* DELETE - Delete agent
|
||||
*/
|
||||
|
||||
const AGENT_CORE_PATH = process.env.AGENT_CORE_PATH || '/app/agent-core'
|
||||
const SOUL_PATH = path.join(AGENT_CORE_PATH, 'soul')
|
||||
|
||||
interface AgentDetails {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
soulFile: string
|
||||
soulContent: string
|
||||
color: string
|
||||
icon: string
|
||||
status: 'running' | 'paused' | 'stopped' | 'error'
|
||||
activeSessions: number
|
||||
totalProcessed: number
|
||||
avgResponseTime: number
|
||||
errorRate: number
|
||||
lastActivity: string
|
||||
version: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// Agent metadata (in production, store in database)
|
||||
const agentMetadata: Record<string, Partial<AgentDetails>> = {
|
||||
'tutor-agent': {
|
||||
name: 'TutorAgent',
|
||||
description: 'Lernbegleitung und Fragen beantworten',
|
||||
color: '#3b82f6',
|
||||
icon: 'brain'
|
||||
},
|
||||
'grader-agent': {
|
||||
name: 'GraderAgent',
|
||||
description: 'Klausur-Korrektur und Bewertung',
|
||||
color: '#10b981',
|
||||
icon: 'bot'
|
||||
},
|
||||
'quality-judge': {
|
||||
name: 'QualityJudge',
|
||||
description: 'BQAS Qualitaetspruefung',
|
||||
color: '#f59e0b',
|
||||
icon: 'settings'
|
||||
},
|
||||
'alert-agent': {
|
||||
name: 'AlertAgent',
|
||||
description: 'Monitoring und Benachrichtigungen',
|
||||
color: '#ef4444',
|
||||
icon: 'alert'
|
||||
},
|
||||
'orchestrator': {
|
||||
name: 'Orchestrator',
|
||||
description: 'Task-Koordination und Routing',
|
||||
color: '#8b5cf6',
|
||||
icon: 'message'
|
||||
}
|
||||
}
|
||||
|
||||
// Read SOUL file content
|
||||
async function readSoulFile(agentId: string): Promise<string | null> {
|
||||
const soulFile = `${agentId}.soul.md`
|
||||
const soulFilePath = path.join(SOUL_PATH, soulFile)
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(soulFilePath, 'utf-8')
|
||||
return content
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Get file stats
|
||||
async function getFileStats(agentId: string): Promise<{ createdAt: string; updatedAt: string } | null> {
|
||||
const soulFile = `${agentId}.soul.md`
|
||||
const soulFilePath = path.join(SOUL_PATH, soulFile)
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(soulFilePath)
|
||||
return {
|
||||
createdAt: stats.birthtime.toISOString(),
|
||||
updatedAt: stats.mtime.toISOString()
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Mock agent stats (in production, query from Redis/PostgreSQL)
|
||||
function getMockStats(agentId: string) {
|
||||
const mockStats: Record<string, { activeSessions: number; totalProcessed: number; avgResponseTime: number; errorRate: number }> = {
|
||||
'tutor-agent': { activeSessions: 12, totalProcessed: 1847, avgResponseTime: 234, errorRate: 0.3 },
|
||||
'grader-agent': { activeSessions: 3, totalProcessed: 456, avgResponseTime: 1205, errorRate: 0.5 },
|
||||
'quality-judge': { activeSessions: 8, totalProcessed: 3291, avgResponseTime: 89, errorRate: 0.1 },
|
||||
'alert-agent': { activeSessions: 1, totalProcessed: 892, avgResponseTime: 45, errorRate: 0.0 },
|
||||
'orchestrator': { activeSessions: 24, totalProcessed: 8934, avgResponseTime: 12, errorRate: 0.2 }
|
||||
}
|
||||
|
||||
return mockStats[agentId] || { activeSessions: 0, totalProcessed: 0, avgResponseTime: 0, errorRate: 0 }
|
||||
}
|
||||
|
||||
// GET - Get agent details
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ agentId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { agentId } = await params
|
||||
|
||||
const soulContent = await readSoulFile(agentId)
|
||||
if (!soulContent) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Agent not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const fileStats = await getFileStats(agentId)
|
||||
const metadata = agentMetadata[agentId] || {}
|
||||
const stats = getMockStats(agentId)
|
||||
|
||||
const agent: AgentDetails = {
|
||||
id: agentId,
|
||||
name: metadata.name || agentId.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''),
|
||||
description: metadata.description || 'Custom agent',
|
||||
soulFile: `${agentId}.soul.md`,
|
||||
soulContent,
|
||||
color: metadata.color || '#6b7280',
|
||||
icon: metadata.icon || 'bot',
|
||||
status: 'running',
|
||||
...stats,
|
||||
lastActivity: 'just now',
|
||||
version: '1.0.0',
|
||||
createdAt: fileStats?.createdAt || new Date().toISOString(),
|
||||
updatedAt: fileStats?.updatedAt || new Date().toISOString()
|
||||
}
|
||||
|
||||
return NextResponse.json(agent)
|
||||
} catch (error) {
|
||||
console.error('Error fetching agent:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to fetch agent' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - Update agent
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ agentId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { agentId } = await params
|
||||
const body = await request.json()
|
||||
const { soulContent, name, description, color, icon } = body
|
||||
|
||||
const soulFile = `${agentId}.soul.md`
|
||||
const soulFilePath = path.join(SOUL_PATH, soulFile)
|
||||
|
||||
// Check if agent exists
|
||||
try {
|
||||
await fs.access(soulFilePath)
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'Agent not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Update SOUL file if content provided
|
||||
if (soulContent) {
|
||||
// Create backup before updating
|
||||
const backupPath = path.join(SOUL_PATH, '.backups', `${agentId}-${Date.now()}.soul.md`)
|
||||
try {
|
||||
await fs.mkdir(path.dirname(backupPath), { recursive: true })
|
||||
const currentContent = await fs.readFile(soulFilePath, 'utf-8')
|
||||
await fs.writeFile(backupPath, currentContent, 'utf-8')
|
||||
} catch {
|
||||
// Backup failed, continue anyway
|
||||
}
|
||||
|
||||
// Write new content
|
||||
await fs.writeFile(soulFilePath, soulContent, 'utf-8')
|
||||
}
|
||||
|
||||
// In production, update metadata in database
|
||||
// For now, just return success
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Agent ${agentId} updated successfully`,
|
||||
agent: {
|
||||
id: agentId,
|
||||
name: name || agentMetadata[agentId]?.name || agentId,
|
||||
description: description || agentMetadata[agentId]?.description || '',
|
||||
soulFile,
|
||||
color: color || agentMetadata[agentId]?.color || '#6b7280',
|
||||
icon: icon || agentMetadata[agentId]?.icon || 'bot',
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error updating agent:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to update agent' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - Delete agent
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ agentId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { agentId } = await params
|
||||
|
||||
// Don't allow deleting core agents
|
||||
const coreAgents = ['tutor-agent', 'grader-agent', 'quality-judge', 'alert-agent', 'orchestrator']
|
||||
if (coreAgents.includes(agentId)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot delete core system agent' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
const soulFile = `${agentId}.soul.md`
|
||||
const soulFilePath = path.join(SOUL_PATH, soulFile)
|
||||
|
||||
// Check if agent exists
|
||||
try {
|
||||
await fs.access(soulFilePath)
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'Agent not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create backup before deleting
|
||||
const backupPath = path.join(SOUL_PATH, '.deleted', `${agentId}-${Date.now()}.soul.md`)
|
||||
try {
|
||||
await fs.mkdir(path.dirname(backupPath), { recursive: true })
|
||||
const content = await fs.readFile(soulFilePath, 'utf-8')
|
||||
await fs.writeFile(backupPath, content, 'utf-8')
|
||||
} catch {
|
||||
// Backup failed, continue anyway
|
||||
}
|
||||
|
||||
// Delete the file
|
||||
await fs.unlink(soulFilePath)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Agent ${agentId} deleted successfully`
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error deleting agent:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to delete agent' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
187
admin-v2/app/api/admin/agents/[agentId]/soul/route.ts
Normal file
187
admin-v2/app/api/admin/agents/[agentId]/soul/route.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
|
||||
/**
|
||||
* Agent SOUL File API
|
||||
*
|
||||
* GET - Get SOUL file content
|
||||
* PUT - Update SOUL file content
|
||||
* GET /history - Get version history
|
||||
*/
|
||||
|
||||
const AGENT_CORE_PATH = process.env.AGENT_CORE_PATH || '/app/agent-core'
|
||||
const SOUL_PATH = path.join(AGENT_CORE_PATH, 'soul')
|
||||
|
||||
interface SoulVersion {
|
||||
version: string
|
||||
timestamp: string
|
||||
content: string
|
||||
author: string
|
||||
changes: string
|
||||
}
|
||||
|
||||
// Read SOUL file content
|
||||
async function readSoulFile(agentId: string): Promise<string | null> {
|
||||
const soulFile = `${agentId}.soul.md`
|
||||
const soulFilePath = path.join(SOUL_PATH, soulFile)
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(soulFilePath, 'utf-8')
|
||||
return content
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Get version history from backup directory
|
||||
async function getVersionHistory(agentId: string): Promise<SoulVersion[]> {
|
||||
const backupDir = path.join(SOUL_PATH, '.backups')
|
||||
const versions: SoulVersion[] = []
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(backupDir)
|
||||
const agentBackups = files.filter(f => f.startsWith(`${agentId}-`) && f.endsWith('.soul.md'))
|
||||
|
||||
for (const file of agentBackups.slice(-10)) { // Last 10 versions
|
||||
const filePath = path.join(backupDir, file)
|
||||
const stats = await fs.stat(filePath)
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
|
||||
// Extract timestamp from filename
|
||||
const match = file.match(/-(\d+)\.soul\.md$/)
|
||||
const timestamp = match ? new Date(parseInt(match[1])).toISOString() : stats.mtime.toISOString()
|
||||
|
||||
versions.push({
|
||||
version: file.replace('.soul.md', ''),
|
||||
timestamp,
|
||||
content,
|
||||
author: 'Admin',
|
||||
changes: 'Manual update'
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by timestamp descending
|
||||
versions.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||
} catch {
|
||||
// No backup directory or can't read
|
||||
}
|
||||
|
||||
return versions
|
||||
}
|
||||
|
||||
// GET - Get SOUL file content
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ agentId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { agentId } = await params
|
||||
const url = new URL(request.url)
|
||||
const includeHistory = url.searchParams.get('history') === 'true'
|
||||
|
||||
const content = await readSoulFile(agentId)
|
||||
if (!content) {
|
||||
return NextResponse.json(
|
||||
{ error: 'SOUL file not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const soulFile = `${agentId}.soul.md`
|
||||
const soulFilePath = path.join(SOUL_PATH, soulFile)
|
||||
const stats = await fs.stat(soulFilePath)
|
||||
|
||||
const response: {
|
||||
agentId: string
|
||||
soulFile: string
|
||||
content: string
|
||||
updatedAt: string
|
||||
size: number
|
||||
history?: SoulVersion[]
|
||||
} = {
|
||||
agentId,
|
||||
soulFile,
|
||||
content,
|
||||
updatedAt: stats.mtime.toISOString(),
|
||||
size: stats.size
|
||||
}
|
||||
|
||||
if (includeHistory) {
|
||||
response.history = await getVersionHistory(agentId)
|
||||
}
|
||||
|
||||
return NextResponse.json(response)
|
||||
} catch (error) {
|
||||
console.error('Error fetching SOUL file:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to fetch SOUL file' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - Update SOUL file content
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ agentId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { agentId } = await params
|
||||
const body = await request.json()
|
||||
const { content, author, changeDescription } = body
|
||||
|
||||
if (!content) {
|
||||
return NextResponse.json(
|
||||
{ error: 'content is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const soulFile = `${agentId}.soul.md`
|
||||
const soulFilePath = path.join(SOUL_PATH, soulFile)
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await fs.access(soulFilePath)
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'SOUL file not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create backup before updating
|
||||
const backupDir = path.join(SOUL_PATH, '.backups')
|
||||
const backupPath = path.join(backupDir, `${agentId}-${Date.now()}.soul.md`)
|
||||
|
||||
try {
|
||||
await fs.mkdir(backupDir, { recursive: true })
|
||||
const currentContent = await fs.readFile(soulFilePath, 'utf-8')
|
||||
await fs.writeFile(backupPath, currentContent, 'utf-8')
|
||||
} catch (backupError) {
|
||||
console.warn('Failed to create backup:', backupError)
|
||||
}
|
||||
|
||||
// Write new content
|
||||
await fs.writeFile(soulFilePath, content, 'utf-8')
|
||||
const stats = await fs.stat(soulFilePath)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `SOUL file for ${agentId} updated successfully`,
|
||||
agentId,
|
||||
soulFile,
|
||||
updatedAt: stats.mtime.toISOString(),
|
||||
size: stats.size,
|
||||
author: author || 'Admin',
|
||||
changeDescription: changeDescription || 'Manual update'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error updating SOUL file:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to update SOUL file' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
282
admin-v2/app/api/admin/agents/route.ts
Normal file
282
admin-v2/app/api/admin/agents/route.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
|
||||
/**
|
||||
* Agent Management API
|
||||
*
|
||||
* GET - List all agents with their status and configuration
|
||||
* POST - Create a new agent (with SOUL file)
|
||||
*/
|
||||
|
||||
const AGENT_CORE_PATH = process.env.AGENT_CORE_PATH || '/app/agent-core'
|
||||
const SOUL_PATH = path.join(AGENT_CORE_PATH, 'soul')
|
||||
|
||||
interface AgentConfig {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
soulFile: string
|
||||
color: string
|
||||
icon: string
|
||||
status: 'running' | 'paused' | 'stopped' | 'error'
|
||||
activeSessions: number
|
||||
totalProcessed: number
|
||||
avgResponseTime: number
|
||||
lastActivity: string
|
||||
version: string
|
||||
}
|
||||
|
||||
interface AgentStats {
|
||||
totalSessions: number
|
||||
activeSessions: number
|
||||
totalMessages: number
|
||||
avgLatency: number
|
||||
errorRate: number
|
||||
memoryUsage: number
|
||||
}
|
||||
|
||||
// Default agent configurations
|
||||
const defaultAgents: AgentConfig[] = [
|
||||
{
|
||||
id: 'tutor-agent',
|
||||
name: 'TutorAgent',
|
||||
description: 'Lernbegleitung und Fragen beantworten',
|
||||
soulFile: 'tutor-agent.soul.md',
|
||||
color: '#3b82f6',
|
||||
icon: 'brain',
|
||||
status: 'running',
|
||||
activeSessions: 0,
|
||||
totalProcessed: 0,
|
||||
avgResponseTime: 0,
|
||||
lastActivity: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
id: 'grader-agent',
|
||||
name: 'GraderAgent',
|
||||
description: 'Klausur-Korrektur und Bewertung',
|
||||
soulFile: 'grader-agent.soul.md',
|
||||
color: '#10b981',
|
||||
icon: 'bot',
|
||||
status: 'running',
|
||||
activeSessions: 0,
|
||||
totalProcessed: 0,
|
||||
avgResponseTime: 0,
|
||||
lastActivity: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
id: 'quality-judge',
|
||||
name: 'QualityJudge',
|
||||
description: 'BQAS Qualitaetspruefung',
|
||||
soulFile: 'quality-judge.soul.md',
|
||||
color: '#f59e0b',
|
||||
icon: 'settings',
|
||||
status: 'running',
|
||||
activeSessions: 0,
|
||||
totalProcessed: 0,
|
||||
avgResponseTime: 0,
|
||||
lastActivity: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
id: 'alert-agent',
|
||||
name: 'AlertAgent',
|
||||
description: 'Monitoring und Benachrichtigungen',
|
||||
soulFile: 'alert-agent.soul.md',
|
||||
color: '#ef4444',
|
||||
icon: 'alert',
|
||||
status: 'running',
|
||||
activeSessions: 0,
|
||||
totalProcessed: 0,
|
||||
avgResponseTime: 0,
|
||||
lastActivity: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
id: 'orchestrator',
|
||||
name: 'Orchestrator',
|
||||
description: 'Task-Koordination und Routing',
|
||||
soulFile: 'orchestrator.soul.md',
|
||||
color: '#8b5cf6',
|
||||
icon: 'message',
|
||||
status: 'running',
|
||||
activeSessions: 0,
|
||||
totalProcessed: 0,
|
||||
avgResponseTime: 0,
|
||||
lastActivity: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
}
|
||||
]
|
||||
|
||||
// Check if SOUL file exists
|
||||
async function checkSoulFile(filename: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(path.join(SOUL_PATH, filename))
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Get agent status from Redis/API (mock for now)
|
||||
async function getAgentStatus(agentId: string): Promise<{
|
||||
status: 'running' | 'paused' | 'stopped' | 'error'
|
||||
activeSessions: number
|
||||
totalProcessed: number
|
||||
avgResponseTime: number
|
||||
lastActivity: string
|
||||
}> {
|
||||
// In production, query Redis/PostgreSQL for real stats
|
||||
// For now, return mock data with some variation
|
||||
const mockStats = {
|
||||
'tutor-agent': { activeSessions: 12, totalProcessed: 1847, avgResponseTime: 234, lastActivity: '2 min ago' },
|
||||
'grader-agent': { activeSessions: 3, totalProcessed: 456, avgResponseTime: 1205, lastActivity: '5 min ago' },
|
||||
'quality-judge': { activeSessions: 8, totalProcessed: 3291, avgResponseTime: 89, lastActivity: '1 min ago' },
|
||||
'alert-agent': { activeSessions: 1, totalProcessed: 892, avgResponseTime: 45, lastActivity: '30 sec ago' },
|
||||
'orchestrator': { activeSessions: 24, totalProcessed: 8934, avgResponseTime: 12, lastActivity: 'just now' }
|
||||
}
|
||||
|
||||
const stats = mockStats[agentId as keyof typeof mockStats] || {
|
||||
activeSessions: 0,
|
||||
totalProcessed: 0,
|
||||
avgResponseTime: 0,
|
||||
lastActivity: 'never'
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'running',
|
||||
...stats
|
||||
}
|
||||
}
|
||||
|
||||
// GET - List all agents
|
||||
export async function GET() {
|
||||
try {
|
||||
const agents: AgentConfig[] = []
|
||||
|
||||
for (const defaultAgent of defaultAgents) {
|
||||
const soulExists = await checkSoulFile(defaultAgent.soulFile)
|
||||
const status = await getAgentStatus(defaultAgent.id)
|
||||
|
||||
agents.push({
|
||||
...defaultAgent,
|
||||
...status,
|
||||
status: soulExists ? status.status : 'error'
|
||||
})
|
||||
}
|
||||
|
||||
// Also check for any additional SOUL files
|
||||
try {
|
||||
const soulFiles = await fs.readdir(SOUL_PATH)
|
||||
for (const file of soulFiles) {
|
||||
if (file.endsWith('.soul.md')) {
|
||||
const agentId = file.replace('.soul.md', '')
|
||||
if (!agents.find(a => a.id === agentId)) {
|
||||
const status = await getAgentStatus(agentId)
|
||||
agents.push({
|
||||
id: agentId,
|
||||
name: agentId.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''),
|
||||
description: 'Custom agent',
|
||||
soulFile: file,
|
||||
color: '#6b7280',
|
||||
icon: 'bot',
|
||||
version: '1.0.0',
|
||||
...status
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// SOUL directory doesn't exist or isn't accessible
|
||||
}
|
||||
|
||||
// Calculate aggregate stats
|
||||
const stats: AgentStats = {
|
||||
totalSessions: agents.reduce((sum, a) => sum + a.totalProcessed, 0),
|
||||
activeSessions: agents.reduce((sum, a) => sum + a.activeSessions, 0),
|
||||
totalMessages: agents.reduce((sum, a) => sum + a.totalProcessed, 0) * 3, // estimate
|
||||
avgLatency: Math.round(agents.reduce((sum, a) => sum + a.avgResponseTime, 0) / agents.length),
|
||||
errorRate: 0.8, // mock
|
||||
memoryUsage: 67 // mock
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
agents,
|
||||
stats,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching agents:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to fetch agents' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Create new agent
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { id, name, description, soulContent, color, icon } = body
|
||||
|
||||
if (!id || !name || !soulContent) {
|
||||
return NextResponse.json(
|
||||
{ error: 'id, name, and soulContent are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate agent ID format
|
||||
if (!/^[a-z0-9-]+$/.test(id)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Agent ID must contain only lowercase letters, numbers, and hyphens' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const soulFile = `${id}.soul.md`
|
||||
const soulFilePath = path.join(SOUL_PATH, soulFile)
|
||||
|
||||
// Check if agent already exists
|
||||
const exists = await checkSoulFile(soulFile)
|
||||
if (exists) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Agent with this ID already exists' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create SOUL file
|
||||
await fs.writeFile(soulFilePath, soulContent, 'utf-8')
|
||||
|
||||
const newAgent: AgentConfig = {
|
||||
id,
|
||||
name,
|
||||
description: description || '',
|
||||
soulFile,
|
||||
color: color || '#6b7280',
|
||||
icon: icon || 'bot',
|
||||
status: 'stopped',
|
||||
activeSessions: 0,
|
||||
totalProcessed: 0,
|
||||
avgResponseTime: 0,
|
||||
lastActivity: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
agent: newAgent,
|
||||
message: `Agent ${name} created successfully`
|
||||
}, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Error creating agent:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to create agent' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
240
admin-v2/app/api/admin/agents/sessions/route.ts
Normal file
240
admin-v2/app/api/admin/agents/sessions/route.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* Agent Sessions API
|
||||
*
|
||||
* GET - List all active agent sessions
|
||||
* POST - Create/start a new session (for testing)
|
||||
*/
|
||||
|
||||
interface AgentSession {
|
||||
id: string
|
||||
agentType: string
|
||||
agentId: string
|
||||
userId: string
|
||||
userName: string
|
||||
state: 'active' | 'paused' | 'completed' | 'failed'
|
||||
createdAt: string
|
||||
lastActivity: string
|
||||
checkpointCount: number
|
||||
messagesProcessed: number
|
||||
currentTask: string | null
|
||||
avgResponseTime: number
|
||||
}
|
||||
|
||||
// Mock sessions data (in production, query from PostgreSQL/Redis)
|
||||
function getMockSessions(): AgentSession[] {
|
||||
const now = new Date()
|
||||
return [
|
||||
{
|
||||
id: `session-${Date.now()}-001`,
|
||||
agentType: 'tutor-agent',
|
||||
agentId: 'tutor-1',
|
||||
userId: 'user-123',
|
||||
userName: 'Max Mustermann',
|
||||
state: 'active',
|
||||
createdAt: new Date(now.getTime() - 75 * 60000).toISOString(),
|
||||
lastActivity: new Date(now.getTime() - 37000).toISOString(),
|
||||
checkpointCount: 5,
|
||||
messagesProcessed: 23,
|
||||
currentTask: 'Erklaere Quadratische Funktionen',
|
||||
avgResponseTime: 245
|
||||
},
|
||||
{
|
||||
id: `session-${Date.now()}-002`,
|
||||
agentType: 'tutor-agent',
|
||||
agentId: 'tutor-2',
|
||||
userId: 'user-456',
|
||||
userName: 'Anna Schmidt',
|
||||
state: 'active',
|
||||
createdAt: new Date(now.getTime() - 45 * 60000).toISOString(),
|
||||
lastActivity: new Date(now.getTime() - 108000).toISOString(),
|
||||
checkpointCount: 3,
|
||||
messagesProcessed: 12,
|
||||
currentTask: 'Hilfe bei Gedichtanalyse',
|
||||
avgResponseTime: 312
|
||||
},
|
||||
{
|
||||
id: `session-${Date.now()}-003`,
|
||||
agentType: 'grader-agent',
|
||||
agentId: 'grader-1',
|
||||
userId: 'user-789',
|
||||
userName: 'Frau Mueller (Lehrerin)',
|
||||
state: 'active',
|
||||
createdAt: new Date(now.getTime() - 105 * 60000).toISOString(),
|
||||
lastActivity: new Date(now.getTime() - 180000).toISOString(),
|
||||
checkpointCount: 12,
|
||||
messagesProcessed: 45,
|
||||
currentTask: 'Korrektur Klausur 10b - Arbeit 7/24',
|
||||
avgResponseTime: 1205
|
||||
},
|
||||
{
|
||||
id: `session-${Date.now()}-004`,
|
||||
agentType: 'quality-judge',
|
||||
agentId: 'judge-1',
|
||||
userId: 'system',
|
||||
userName: 'System (BQAS)',
|
||||
state: 'active',
|
||||
createdAt: new Date(now.getTime() - 465 * 60000).toISOString(),
|
||||
lastActivity: new Date(now.getTime() - 1000).toISOString(),
|
||||
checkpointCount: 156,
|
||||
messagesProcessed: 892,
|
||||
currentTask: 'Quality Check Queue Processing',
|
||||
avgResponseTime: 89
|
||||
},
|
||||
{
|
||||
id: `session-${Date.now()}-005`,
|
||||
agentType: 'orchestrator',
|
||||
agentId: 'orchestrator-main',
|
||||
userId: 'system',
|
||||
userName: 'System',
|
||||
state: 'active',
|
||||
createdAt: new Date(now.getTime() - 945 * 60000).toISOString(),
|
||||
lastActivity: now.toISOString(),
|
||||
checkpointCount: 2341,
|
||||
messagesProcessed: 8934,
|
||||
currentTask: 'Routing incoming requests',
|
||||
avgResponseTime: 12
|
||||
},
|
||||
{
|
||||
id: `session-${Date.now()}-006`,
|
||||
agentType: 'tutor-agent',
|
||||
agentId: 'tutor-3',
|
||||
userId: 'user-101',
|
||||
userName: 'Tim Berger',
|
||||
state: 'paused',
|
||||
createdAt: new Date(now.getTime() - 150 * 60000).toISOString(),
|
||||
lastActivity: new Date(now.getTime() - 90 * 60000).toISOString(),
|
||||
checkpointCount: 8,
|
||||
messagesProcessed: 34,
|
||||
currentTask: null,
|
||||
avgResponseTime: 278
|
||||
},
|
||||
{
|
||||
id: `session-${Date.now()}-007`,
|
||||
agentType: 'grader-agent',
|
||||
agentId: 'grader-2',
|
||||
userId: 'user-202',
|
||||
userName: 'Herr Weber (Lehrer)',
|
||||
state: 'completed',
|
||||
createdAt: new Date(now.getTime() - 345 * 60000).toISOString(),
|
||||
lastActivity: new Date(now.getTime() - 225 * 60000).toISOString(),
|
||||
checkpointCount: 24,
|
||||
messagesProcessed: 120,
|
||||
currentTask: null,
|
||||
avgResponseTime: 1102
|
||||
},
|
||||
{
|
||||
id: `session-${Date.now()}-008`,
|
||||
agentType: 'alert-agent',
|
||||
agentId: 'alert-1',
|
||||
userId: 'system',
|
||||
userName: 'System (Monitoring)',
|
||||
state: 'active',
|
||||
createdAt: new Date(now.getTime() - 945 * 60000).toISOString(),
|
||||
lastActivity: new Date(now.getTime() - 2000).toISOString(),
|
||||
checkpointCount: 48,
|
||||
messagesProcessed: 256,
|
||||
currentTask: 'Monitoring System Health',
|
||||
avgResponseTime: 45
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// GET - List all sessions
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const url = new URL(request.url)
|
||||
const state = url.searchParams.get('state')
|
||||
const agentType = url.searchParams.get('agentType')
|
||||
const limit = parseInt(url.searchParams.get('limit') || '100')
|
||||
const offset = parseInt(url.searchParams.get('offset') || '0')
|
||||
|
||||
let sessions = getMockSessions()
|
||||
|
||||
// Apply filters
|
||||
if (state) {
|
||||
sessions = sessions.filter(s => s.state === state)
|
||||
}
|
||||
if (agentType) {
|
||||
sessions = sessions.filter(s => s.agentType === agentType)
|
||||
}
|
||||
|
||||
// Calculate stats
|
||||
const stats = {
|
||||
total: sessions.length,
|
||||
active: sessions.filter(s => s.state === 'active').length,
|
||||
paused: sessions.filter(s => s.state === 'paused').length,
|
||||
completed: sessions.filter(s => s.state === 'completed').length,
|
||||
failed: sessions.filter(s => s.state === 'failed').length,
|
||||
totalMessages: sessions.reduce((sum, s) => sum + s.messagesProcessed, 0),
|
||||
avgResponseTime: Math.round(
|
||||
sessions.reduce((sum, s) => sum + s.avgResponseTime, 0) / sessions.length
|
||||
)
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
const paginatedSessions = sessions.slice(offset, offset + limit)
|
||||
|
||||
return NextResponse.json({
|
||||
sessions: paginatedSessions,
|
||||
stats,
|
||||
pagination: {
|
||||
total: sessions.length,
|
||||
limit,
|
||||
offset,
|
||||
hasMore: offset + limit < sessions.length
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching sessions:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to fetch sessions' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Create new session (for testing)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { agentType, userId, userName, context } = body
|
||||
|
||||
if (!agentType || !userId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'agentType and userId are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// In production, create session via agent-core SessionManager
|
||||
const newSession: AgentSession = {
|
||||
id: `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
agentType,
|
||||
agentId: `${agentType.replace('-agent', '')}-${Math.floor(Math.random() * 100)}`,
|
||||
userId,
|
||||
userName: userName || userId,
|
||||
state: 'active',
|
||||
createdAt: new Date().toISOString(),
|
||||
lastActivity: new Date().toISOString(),
|
||||
checkpointCount: 0,
|
||||
messagesProcessed: 0,
|
||||
currentTask: context?.task || null,
|
||||
avgResponseTime: 0
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
session: newSession,
|
||||
message: 'Session created successfully'
|
||||
}, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Error creating session:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to create session' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
208
admin-v2/app/api/admin/agents/statistics/route.ts
Normal file
208
admin-v2/app/api/admin/agents/statistics/route.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* Agent Statistics API
|
||||
*
|
||||
* GET - Get aggregated statistics for all agents
|
||||
*/
|
||||
|
||||
interface AgentMetric {
|
||||
agentType: string
|
||||
name: string
|
||||
color: string
|
||||
sessions: number
|
||||
messagesProcessed: number
|
||||
avgResponseTime: number
|
||||
errorRate: number
|
||||
successRate: number
|
||||
trend: 'up' | 'down' | 'stable'
|
||||
trendValue: number
|
||||
}
|
||||
|
||||
interface DailyStats {
|
||||
date: string
|
||||
sessions: number
|
||||
messages: number
|
||||
errors: number
|
||||
avgLatency: number
|
||||
}
|
||||
|
||||
interface HourlyLatency {
|
||||
timestamp: string
|
||||
value: number
|
||||
}
|
||||
|
||||
// Mock agent metrics
|
||||
function getAgentMetrics(): AgentMetric[] {
|
||||
return [
|
||||
{
|
||||
agentType: 'tutor-agent',
|
||||
name: 'TutorAgent',
|
||||
color: '#3b82f6',
|
||||
sessions: 156,
|
||||
messagesProcessed: 4521,
|
||||
avgResponseTime: 234,
|
||||
errorRate: 0.3,
|
||||
successRate: 99.7,
|
||||
trend: 'up',
|
||||
trendValue: 12
|
||||
},
|
||||
{
|
||||
agentType: 'grader-agent',
|
||||
name: 'GraderAgent',
|
||||
color: '#10b981',
|
||||
sessions: 45,
|
||||
messagesProcessed: 1205,
|
||||
avgResponseTime: 1102,
|
||||
errorRate: 0.5,
|
||||
successRate: 99.5,
|
||||
trend: 'stable',
|
||||
trendValue: 2
|
||||
},
|
||||
{
|
||||
agentType: 'quality-judge',
|
||||
name: 'QualityJudge',
|
||||
color: '#f59e0b',
|
||||
sessions: 89,
|
||||
messagesProcessed: 8934,
|
||||
avgResponseTime: 89,
|
||||
errorRate: 0.1,
|
||||
successRate: 99.9,
|
||||
trend: 'up',
|
||||
trendValue: 8
|
||||
},
|
||||
{
|
||||
agentType: 'alert-agent',
|
||||
name: 'AlertAgent',
|
||||
color: '#ef4444',
|
||||
sessions: 12,
|
||||
messagesProcessed: 892,
|
||||
avgResponseTime: 45,
|
||||
errorRate: 0.0,
|
||||
successRate: 100,
|
||||
trend: 'stable',
|
||||
trendValue: 0
|
||||
},
|
||||
{
|
||||
agentType: 'orchestrator',
|
||||
name: 'Orchestrator',
|
||||
color: '#8b5cf6',
|
||||
sessions: 234,
|
||||
messagesProcessed: 15420,
|
||||
avgResponseTime: 12,
|
||||
errorRate: 0.2,
|
||||
successRate: 99.8,
|
||||
trend: 'up',
|
||||
trendValue: 15
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Generate mock daily stats for the last N days
|
||||
function getDailyStats(days: number): DailyStats[] {
|
||||
const stats: DailyStats[] = []
|
||||
const now = new Date()
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date(now)
|
||||
date.setDate(date.getDate() - i)
|
||||
|
||||
stats.push({
|
||||
date: date.toISOString().split('T')[0],
|
||||
sessions: 400 + Math.floor(Math.random() * 150),
|
||||
messages: 11000 + Math.floor(Math.random() * 5000),
|
||||
errors: 8 + Math.floor(Math.random() * 12),
|
||||
avgLatency: 140 + Math.floor(Math.random() * 25)
|
||||
})
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// Generate hourly latency data for the last 24 hours
|
||||
function getHourlyLatency(): HourlyLatency[] {
|
||||
const data: HourlyLatency[] = []
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
data.push({
|
||||
timestamp: `${i.toString().padStart(2, '0')}:00`,
|
||||
value: 100 + Math.floor(Math.random() * 100)
|
||||
})
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// GET - Get statistics
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const url = new URL(request.url)
|
||||
const timeRange = url.searchParams.get('range') || '7d'
|
||||
|
||||
// Determine days based on time range
|
||||
const days = timeRange === '24h' ? 1 : timeRange === '7d' ? 7 : 30
|
||||
|
||||
const agentMetrics = getAgentMetrics()
|
||||
const dailyStats = getDailyStats(days)
|
||||
const hourlyLatency = getHourlyLatency()
|
||||
|
||||
// Calculate totals
|
||||
const totals = {
|
||||
sessions: agentMetrics.reduce((sum, m) => sum + m.sessions, 0),
|
||||
messages: agentMetrics.reduce((sum, m) => sum + m.messagesProcessed, 0),
|
||||
avgLatency: Math.round(
|
||||
agentMetrics.reduce((sum, m) => sum + m.avgResponseTime, 0) / agentMetrics.length
|
||||
),
|
||||
avgErrorRate: parseFloat(
|
||||
(agentMetrics.reduce((sum, m) => sum + m.errorRate, 0) / agentMetrics.length).toFixed(2)
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate week totals from daily stats
|
||||
const weekTotals = {
|
||||
sessions: dailyStats.reduce((sum, d) => sum + d.sessions, 0),
|
||||
messages: dailyStats.reduce((sum, d) => sum + d.messages, 0),
|
||||
errors: dailyStats.reduce((sum, d) => sum + d.errors, 0),
|
||||
avgLatency: Math.round(
|
||||
dailyStats.reduce((sum, d) => sum + d.avgLatency, 0) / dailyStats.length
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate trends
|
||||
const trends = {
|
||||
sessions: {
|
||||
value: 12,
|
||||
direction: 'up' as const
|
||||
},
|
||||
messages: {
|
||||
value: 8,
|
||||
direction: 'up' as const
|
||||
},
|
||||
latency: {
|
||||
value: 5,
|
||||
direction: 'down' as const // down is good for latency
|
||||
},
|
||||
errors: {
|
||||
value: 3,
|
||||
direction: 'up' as const
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
agentMetrics,
|
||||
dailyStats,
|
||||
hourlyLatency,
|
||||
totals,
|
||||
weekTotals,
|
||||
trends,
|
||||
timeRange,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching statistics:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to fetch statistics' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Complete Audit Session API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
// Complete audit session (in_progress -> completed)
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { sessionId } = await params
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/sessions/${sessionId}/complete`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Complete audit session proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Audit Session PDF Report API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
// Generate PDF report for audit session
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { sessionId } = await params
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
const { searchParams } = new URL(request.url)
|
||||
|
||||
const language = searchParams.get('language') || 'de'
|
||||
const includeSignatures = searchParams.get('include_signatures') !== 'false'
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
language,
|
||||
include_signatures: String(includeSignatures)
|
||||
})
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/compliance/audit/sessions/${sessionId}/report/pdf?${queryParams}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
signal: AbortSignal.timeout(60000) // 60s timeout for PDF generation
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
// Stream the PDF response
|
||||
const pdfBuffer = await response.arrayBuffer()
|
||||
const contentDisposition = response.headers.get('Content-Disposition') ||
|
||||
`attachment; filename=audit-report-${sessionId}.pdf`
|
||||
|
||||
return new NextResponse(pdfBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': contentDisposition,
|
||||
'Content-Length': String(pdfBuffer.byteLength)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Audit PDF proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'PDF-Generierung fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
80
admin-v2/app/api/admin/audit/sessions/[sessionId]/route.ts
Normal file
80
admin-v2/app/api/admin/audit/sessions/[sessionId]/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Audit Session Detail API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
// Get audit session detail
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { sessionId } = await params
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/sessions/${sessionId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Audit session detail proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete audit session
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { sessionId } = await params
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/sessions/${sessionId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Delete audit session proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Start Audit Session API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
// Start audit session (draft -> in_progress)
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { sessionId } = await params
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/sessions/${sessionId}/start`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Start audit session proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
82
admin-v2/app/api/admin/audit/sessions/route.ts
Normal file
82
admin-v2/app/api/admin/audit/sessions/route.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Audit Sessions API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
// Get all audit sessions
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
const { searchParams } = new URL(request.url)
|
||||
|
||||
const status = searchParams.get('status')
|
||||
const queryParams = new URLSearchParams()
|
||||
if (status) queryParams.set('status', status)
|
||||
|
||||
const url = `${BACKEND_URL}/api/v1/compliance/audit/sessions${queryParams.toString() ? `?${queryParams}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Audit sessions proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Create new audit session
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/sessions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Create audit session proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
210
admin-v2/app/api/admin/communication/stats/route.ts
Normal file
210
admin-v2/app/api/admin/communication/stats/route.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Communication Admin API Route - Stats Proxy
|
||||
*
|
||||
* Proxies requests to Matrix/Jitsi admin endpoints via backend
|
||||
* Aggregates statistics from both services
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
// Service URLs
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
|
||||
const CONSENT_SERVICE_URL = process.env.CONSENT_SERVICE_URL || 'http://localhost:8081'
|
||||
const MATRIX_ADMIN_URL = process.env.MATRIX_ADMIN_URL || 'http://localhost:8448'
|
||||
const JITSI_URL = process.env.JITSI_URL || 'http://localhost:8443'
|
||||
|
||||
// Matrix Admin Token (for Synapse Admin API)
|
||||
const MATRIX_ADMIN_TOKEN = process.env.MATRIX_ADMIN_TOKEN || ''
|
||||
|
||||
interface MatrixStats {
|
||||
total_users: number
|
||||
active_users: number
|
||||
total_rooms: number
|
||||
active_rooms: number
|
||||
messages_today: number
|
||||
messages_this_week: number
|
||||
status: 'online' | 'offline' | 'degraded'
|
||||
}
|
||||
|
||||
interface JitsiStats {
|
||||
active_meetings: number
|
||||
total_participants: number
|
||||
meetings_today: number
|
||||
average_duration_minutes: number
|
||||
peak_concurrent_users: number
|
||||
total_minutes_today: number
|
||||
status: 'online' | 'offline' | 'degraded'
|
||||
}
|
||||
|
||||
async function fetchFromBackend(): Promise<{
|
||||
matrix: MatrixStats
|
||||
jitsi: JitsiStats
|
||||
active_meetings: unknown[]
|
||||
recent_rooms: unknown[]
|
||||
} | null> {
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/communication/admin/stats`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
if (response.ok) {
|
||||
return await response.json()
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Backend not reachable, trying consent service:', error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function fetchFromConsentService(): Promise<{
|
||||
matrix: MatrixStats
|
||||
jitsi: JitsiStats
|
||||
active_meetings: unknown[]
|
||||
recent_rooms: unknown[]
|
||||
} | null> {
|
||||
try {
|
||||
const response = await fetch(`${CONSENT_SERVICE_URL}/api/v1/communication/admin/stats`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
if (response.ok) {
|
||||
return await response.json()
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Consent service not reachable:', error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function fetchMatrixStats(): Promise<MatrixStats> {
|
||||
try {
|
||||
// Check if Matrix is reachable
|
||||
const healthCheck = await fetch(`${MATRIX_ADMIN_URL}/_matrix/client/versions`, {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
})
|
||||
|
||||
if (healthCheck.ok) {
|
||||
// Try to get user count from admin API
|
||||
if (MATRIX_ADMIN_TOKEN) {
|
||||
try {
|
||||
const usersResponse = await fetch(`${MATRIX_ADMIN_URL}/_synapse/admin/v2/users?limit=1`, {
|
||||
headers: { 'Authorization': `Bearer ${MATRIX_ADMIN_TOKEN}` },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
if (usersResponse.ok) {
|
||||
const data = await usersResponse.json()
|
||||
return {
|
||||
total_users: data.total || 0,
|
||||
active_users: 0,
|
||||
total_rooms: 0,
|
||||
active_rooms: 0,
|
||||
messages_today: 0,
|
||||
messages_this_week: 0,
|
||||
status: 'online'
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Admin API not available
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total_users: 0,
|
||||
active_users: 0,
|
||||
total_rooms: 0,
|
||||
active_rooms: 0,
|
||||
messages_today: 0,
|
||||
messages_this_week: 0,
|
||||
status: 'degraded' // Server reachable but no admin access
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Matrix stats fetch error:', error)
|
||||
}
|
||||
|
||||
return {
|
||||
total_users: 0,
|
||||
active_users: 0,
|
||||
total_rooms: 0,
|
||||
active_rooms: 0,
|
||||
messages_today: 0,
|
||||
messages_this_week: 0,
|
||||
status: 'offline'
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchJitsiStats(): Promise<JitsiStats> {
|
||||
try {
|
||||
// Check if Jitsi is reachable
|
||||
const healthCheck = await fetch(`${JITSI_URL}/http-bind`, {
|
||||
method: 'HEAD',
|
||||
signal: AbortSignal.timeout(5000)
|
||||
})
|
||||
|
||||
return {
|
||||
active_meetings: 0,
|
||||
total_participants: 0,
|
||||
meetings_today: 0,
|
||||
average_duration_minutes: 0,
|
||||
peak_concurrent_users: 0,
|
||||
total_minutes_today: 0,
|
||||
status: healthCheck.ok ? 'online' : 'offline'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Jitsi stats fetch error:', error)
|
||||
return {
|
||||
active_meetings: 0,
|
||||
total_participants: 0,
|
||||
meetings_today: 0,
|
||||
average_duration_minutes: 0,
|
||||
peak_concurrent_users: 0,
|
||||
total_minutes_today: 0,
|
||||
status: 'offline'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Try backend first
|
||||
let data = await fetchFromBackend()
|
||||
|
||||
// Fallback to consent service
|
||||
if (!data) {
|
||||
data = await fetchFromConsentService()
|
||||
}
|
||||
|
||||
// If both fail, try direct service checks
|
||||
if (!data) {
|
||||
const [matrixStats, jitsiStats] = await Promise.all([
|
||||
fetchMatrixStats(),
|
||||
fetchJitsiStats()
|
||||
])
|
||||
|
||||
data = {
|
||||
matrix: matrixStats,
|
||||
jitsi: jitsiStats,
|
||||
active_meetings: [],
|
||||
recent_rooms: []
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
...data,
|
||||
last_updated: new Date().toISOString()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Communication stats error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Fehler beim Abrufen der Statistiken',
|
||||
matrix: { status: 'offline', total_users: 0, active_users: 0, total_rooms: 0, active_rooms: 0, messages_today: 0, messages_this_week: 0 },
|
||||
jitsi: { status: 'offline', active_meetings: 0, total_participants: 0, meetings_today: 0, average_duration_minutes: 0, peak_concurrent_users: 0, total_minutes_today: 0 },
|
||||
active_meetings: [],
|
||||
recent_rooms: [],
|
||||
last_updated: new Date().toISOString()
|
||||
},
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
129
admin-v2/app/api/admin/companion/feedback/route.ts
Normal file
129
admin-v2/app/api/admin/companion/feedback/route.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* POST /api/admin/companion/feedback
|
||||
* Submit feedback (bug report, feature request, general feedback)
|
||||
* Proxy to backend /api/feedback
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate required fields
|
||||
if (!body.type || !body.title || !body.description) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Missing required fields: type, title, description',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate feedback type
|
||||
const validTypes = ['bug', 'feature', 'feedback']
|
||||
if (!validTypes.includes(body.type)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid feedback type. Must be: bug, feature, or feedback',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Replace with actual backend call
|
||||
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
// const response = await fetch(`${backendUrl}/api/feedback`, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// // Add auth headers
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// type: body.type,
|
||||
// title: body.title,
|
||||
// description: body.description,
|
||||
// screenshot: body.screenshot,
|
||||
// sessionId: body.sessionId,
|
||||
// metadata: {
|
||||
// ...body.metadata,
|
||||
// source: 'companion',
|
||||
// timestamp: new Date().toISOString(),
|
||||
// userAgent: request.headers.get('user-agent'),
|
||||
// },
|
||||
// }),
|
||||
// })
|
||||
//
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`Backend responded with ${response.status}`)
|
||||
// }
|
||||
//
|
||||
// const data = await response.json()
|
||||
// return NextResponse.json(data)
|
||||
|
||||
// Mock response - just acknowledge the submission
|
||||
const feedbackId = `fb-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
console.log('Feedback received:', {
|
||||
id: feedbackId,
|
||||
type: body.type,
|
||||
title: body.title,
|
||||
description: body.description.substring(0, 100) + '...',
|
||||
hasScreenshot: !!body.screenshot,
|
||||
sessionId: body.sessionId,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Feedback submitted successfully',
|
||||
data: {
|
||||
feedbackId,
|
||||
submittedAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Submit feedback error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/admin/companion/feedback
|
||||
* Get feedback history (admin only)
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const type = searchParams.get('type')
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
|
||||
// TODO: Replace with actual backend call
|
||||
|
||||
// Mock response - empty list for now
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
feedback: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
limit,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Get feedback error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
194
admin-v2/app/api/admin/companion/lesson/route.ts
Normal file
194
admin-v2/app/api/admin/companion/lesson/route.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* POST /api/admin/companion/lesson
|
||||
* Start a new lesson session
|
||||
* Proxy to backend /api/classroom/sessions
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
// TODO: Replace with actual backend call
|
||||
// const response = await fetch(`${backendUrl}/api/classroom/sessions`, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// body: JSON.stringify(body),
|
||||
// })
|
||||
//
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`Backend responded with ${response.status}`)
|
||||
// }
|
||||
//
|
||||
// const data = await response.json()
|
||||
// return NextResponse.json(data)
|
||||
|
||||
// Mock response - create a new session
|
||||
const sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
const mockSession = {
|
||||
success: true,
|
||||
data: {
|
||||
sessionId,
|
||||
classId: body.classId,
|
||||
className: body.className || body.classId,
|
||||
subject: body.subject,
|
||||
topic: body.topic,
|
||||
startTime: new Date().toISOString(),
|
||||
phases: [
|
||||
{ phase: 'einstieg', duration: 8, status: 'active', actualTime: 0 },
|
||||
{ phase: 'erarbeitung', duration: 20, status: 'planned', actualTime: 0 },
|
||||
{ phase: 'sicherung', duration: 10, status: 'planned', actualTime: 0 },
|
||||
{ phase: 'transfer', duration: 7, status: 'planned', actualTime: 0 },
|
||||
{ phase: 'reflexion', duration: 5, status: 'planned', actualTime: 0 },
|
||||
],
|
||||
totalPlannedDuration: 50,
|
||||
currentPhaseIndex: 0,
|
||||
elapsedTime: 0,
|
||||
isPaused: false,
|
||||
pauseDuration: 0,
|
||||
overtimeMinutes: 0,
|
||||
status: 'in_progress',
|
||||
homeworkList: [],
|
||||
materials: [],
|
||||
},
|
||||
}
|
||||
|
||||
return NextResponse.json(mockSession)
|
||||
} catch (error) {
|
||||
console.error('Start lesson error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/admin/companion/lesson
|
||||
* Get current lesson session or list of recent sessions
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const sessionId = searchParams.get('sessionId')
|
||||
|
||||
// TODO: Replace with actual backend call
|
||||
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
// const url = sessionId
|
||||
// ? `${backendUrl}/api/classroom/sessions/${sessionId}`
|
||||
// : `${backendUrl}/api/classroom/sessions`
|
||||
//
|
||||
// const response = await fetch(url)
|
||||
// const data = await response.json()
|
||||
// return NextResponse.json(data)
|
||||
|
||||
// Mock response
|
||||
if (sessionId) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: null, // No active session stored on server in mock
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
sessions: [], // Empty list for now
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Get lesson error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/admin/companion/lesson
|
||||
* Update lesson session (timer state, phase changes, etc.)
|
||||
*/
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { sessionId, ...updates } = body
|
||||
|
||||
if (!sessionId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Session ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Replace with actual backend call
|
||||
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
// const response = await fetch(`${backendUrl}/api/classroom/sessions/${sessionId}`, {
|
||||
// method: 'PATCH',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify(updates),
|
||||
// })
|
||||
//
|
||||
// const data = await response.json()
|
||||
// return NextResponse.json(data)
|
||||
|
||||
// Mock response - just acknowledge the update
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Session updated',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Update lesson error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/admin/companion/lesson
|
||||
* End/delete a lesson session
|
||||
*/
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const sessionId = searchParams.get('sessionId')
|
||||
|
||||
if (!sessionId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Session ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Replace with actual backend call
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Session ended',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('End lesson error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
102
admin-v2/app/api/admin/companion/route.ts
Normal file
102
admin-v2/app/api/admin/companion/route.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* GET /api/admin/companion
|
||||
* Proxy to backend /api/state/dashboard for companion dashboard data
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
// TODO: Replace with actual backend call when endpoint is available
|
||||
// const response = await fetch(`${backendUrl}/api/state/dashboard`, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`Backend responded with ${response.status}`)
|
||||
// }
|
||||
//
|
||||
// const data = await response.json()
|
||||
// return NextResponse.json(data)
|
||||
|
||||
// Mock response for development
|
||||
const mockData = {
|
||||
success: true,
|
||||
data: {
|
||||
context: {
|
||||
currentPhase: 'erarbeitung',
|
||||
phaseDisplayName: 'Erarbeitung',
|
||||
},
|
||||
stats: {
|
||||
classesCount: 4,
|
||||
studentsCount: 96,
|
||||
learningUnitsCreated: 23,
|
||||
gradesEntered: 156,
|
||||
},
|
||||
phases: [
|
||||
{ id: 'einstieg', shortName: 'E', displayName: 'Einstieg', duration: 8, status: 'completed', color: '#4A90E2' },
|
||||
{ id: 'erarbeitung', shortName: 'A', displayName: 'Erarbeitung', duration: 20, status: 'active', color: '#F5A623' },
|
||||
{ id: 'sicherung', shortName: 'S', displayName: 'Sicherung', duration: 10, status: 'planned', color: '#7ED321' },
|
||||
{ id: 'transfer', shortName: 'T', displayName: 'Transfer', duration: 7, status: 'planned', color: '#9013FE' },
|
||||
{ id: 'reflexion', shortName: 'R', displayName: 'Reflexion', duration: 5, status: 'planned', color: '#6B7280' },
|
||||
],
|
||||
progress: {
|
||||
percentage: 65,
|
||||
completed: 13,
|
||||
total: 20,
|
||||
},
|
||||
suggestions: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Klausuren korrigieren',
|
||||
description: 'Deutsch LK - 12 unkorrigierte Arbeiten warten',
|
||||
priority: 'urgent',
|
||||
icon: 'ClipboardCheck',
|
||||
actionTarget: '/ai/klausur-korrektur',
|
||||
estimatedTime: 120,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Elternsprechtag vorbereiten',
|
||||
description: 'Notenuebersicht fuer 8b erstellen',
|
||||
priority: 'high',
|
||||
icon: 'Users',
|
||||
actionTarget: '/education/grades',
|
||||
estimatedTime: 30,
|
||||
},
|
||||
],
|
||||
upcomingEvents: [
|
||||
{
|
||||
id: 'e1',
|
||||
title: 'Mathe-Test 9b',
|
||||
date: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
type: 'exam',
|
||||
inDays: 2,
|
||||
},
|
||||
{
|
||||
id: 'e2',
|
||||
title: 'Elternsprechtag',
|
||||
date: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
type: 'parent_meeting',
|
||||
inDays: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
return NextResponse.json(mockData)
|
||||
} catch (error) {
|
||||
console.error('Companion dashboard error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
137
admin-v2/app/api/admin/companion/settings/route.ts
Normal file
137
admin-v2/app/api/admin/companion/settings/route.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
defaultPhaseDurations: {
|
||||
einstieg: 8,
|
||||
erarbeitung: 20,
|
||||
sicherung: 10,
|
||||
transfer: 7,
|
||||
reflexion: 5,
|
||||
},
|
||||
preferredLessonLength: 45,
|
||||
autoAdvancePhases: true,
|
||||
soundNotifications: true,
|
||||
showKeyboardShortcuts: true,
|
||||
highContrastMode: false,
|
||||
onboardingCompleted: false,
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/admin/companion/settings
|
||||
* Get teacher settings
|
||||
* Proxy to backend /api/teacher/settings
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
// TODO: Replace with actual backend call
|
||||
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
// const response = await fetch(`${backendUrl}/api/teacher/settings`, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// // Add auth headers
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`Backend responded with ${response.status}`)
|
||||
// }
|
||||
//
|
||||
// const data = await response.json()
|
||||
// return NextResponse.json(data)
|
||||
|
||||
// Mock response - return default settings
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: DEFAULT_SETTINGS,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Get settings error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/admin/companion/settings
|
||||
* Update teacher settings
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate the settings structure
|
||||
if (!body || typeof body !== 'object') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid settings data' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Replace with actual backend call
|
||||
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
// const response = await fetch(`${backendUrl}/api/teacher/settings`, {
|
||||
// method: 'PUT',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// // Add auth headers
|
||||
// },
|
||||
// body: JSON.stringify(body),
|
||||
// })
|
||||
//
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`Backend responded with ${response.status}`)
|
||||
// }
|
||||
//
|
||||
// const data = await response.json()
|
||||
// return NextResponse.json(data)
|
||||
|
||||
// Mock response - just acknowledge the save
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Settings saved',
|
||||
data: body,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Save settings error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/admin/companion/settings
|
||||
* Partially update teacher settings
|
||||
*/
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// TODO: Replace with actual backend call
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Settings updated',
|
||||
data: body,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Update settings error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Audit Sign-off API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
// Sign off an item
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string; requirementId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { sessionId, requirementId } = await params
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/compliance/audit/checklist/${sessionId}/items/${requirementId}/sign-off`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Audit sign-off proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Get sign-off status
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string; requirementId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { sessionId, requirementId } = await params
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/compliance/audit/checklist/${sessionId}/items/${requirementId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Get sign-off proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Audit Checklist API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { sessionId } = await params
|
||||
const { searchParams } = new URL(request.url)
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
const page = searchParams.get('page')
|
||||
const page_size = searchParams.get('page_size')
|
||||
const status_filter = searchParams.get('status_filter')
|
||||
const regulation_filter = searchParams.get('regulation_filter')
|
||||
const search = searchParams.get('search')
|
||||
|
||||
if (page) queryParams.set('page', page)
|
||||
if (page_size) queryParams.set('page_size', page_size)
|
||||
if (status_filter) queryParams.set('status_filter', status_filter)
|
||||
if (regulation_filter) queryParams.set('regulation_filter', regulation_filter)
|
||||
if (search) queryParams.set('search', search)
|
||||
|
||||
const url = `${BACKEND_URL}/api/v1/compliance/audit/checklist/${sessionId}${queryParams.toString() ? `?${queryParams}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Audit checklist proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Control Review API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ controlId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { controlId } = await params
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/compliance/controls/${controlId}/review`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Control review proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
54
admin-v2/app/api/admin/compliance/controls/route.ts
Normal file
54
admin-v2/app/api/admin/compliance/controls/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Compliance Controls API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
// Forward query parameters
|
||||
const domain = searchParams.get('domain')
|
||||
const status = searchParams.get('status')
|
||||
const search = searchParams.get('search')
|
||||
const page = searchParams.get('page')
|
||||
const page_size = searchParams.get('page_size')
|
||||
|
||||
if (domain) queryParams.set('domain', domain)
|
||||
if (status) queryParams.set('status', status)
|
||||
if (search) queryParams.set('search', search)
|
||||
if (page) queryParams.set('page', page)
|
||||
if (page_size) queryParams.set('page_size', page_size)
|
||||
|
||||
const url = `${BACKEND_URL}/api/v1/compliance/controls${queryParams.toString() ? `?${queryParams}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Compliance controls proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen', controls: [] },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Compliance Executive Dashboard API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/dashboard/executive`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Executive dashboard proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
36
admin-v2/app/api/admin/compliance/dashboard/route.ts
Normal file
36
admin-v2/app/api/admin/compliance/dashboard/route.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Compliance Dashboard API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/dashboard`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Compliance dashboard proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
81
admin-v2/app/api/admin/compliance/evidence/route.ts
Normal file
81
admin-v2/app/api/admin/compliance/evidence/route.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Compliance Evidence API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
const control_id = searchParams.get('control_id')
|
||||
const evidence_type = searchParams.get('evidence_type')
|
||||
const status = searchParams.get('status')
|
||||
|
||||
if (control_id) queryParams.set('control_id', control_id)
|
||||
if (evidence_type) queryParams.set('evidence_type', evidence_type)
|
||||
if (status) queryParams.set('status', status)
|
||||
|
||||
const url = `${BACKEND_URL}/api/v1/compliance/evidence${queryParams.toString() ? `?${queryParams}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Compliance evidence proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen', evidence: [] },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/evidence`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Create evidence proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
53
admin-v2/app/api/admin/compliance/evidence/upload/route.ts
Normal file
53
admin-v2/app/api/admin/compliance/evidence/upload/route.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Evidence Upload API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
const control_id = searchParams.get('control_id')
|
||||
const evidence_type = searchParams.get('evidence_type')
|
||||
const title = searchParams.get('title')
|
||||
const description = searchParams.get('description')
|
||||
|
||||
if (control_id) queryParams.set('control_id', control_id)
|
||||
if (evidence_type) queryParams.set('evidence_type', evidence_type)
|
||||
if (title) queryParams.set('title', title)
|
||||
if (description) queryParams.set('description', description)
|
||||
|
||||
// Forward the FormData directly
|
||||
const formData = await request.formData()
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/compliance/evidence/upload?${queryParams}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
signal: AbortSignal.timeout(60000) // 60 seconds for file upload
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Evidence upload proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Compliance Module Detail API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ moduleId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { moduleId } = await params
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/compliance/modules/${moduleId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Module detail proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
36
admin-v2/app/api/admin/compliance/modules/overview/route.ts
Normal file
36
admin-v2/app/api/admin/compliance/modules/overview/route.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Compliance Modules Overview API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/modules/overview`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Modules overview proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
51
admin-v2/app/api/admin/compliance/modules/route.ts
Normal file
51
admin-v2/app/api/admin/compliance/modules/route.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Compliance Modules API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
const service_type = searchParams.get('service_type')
|
||||
const criticality = searchParams.get('criticality')
|
||||
const processes_pii = searchParams.get('processes_pii')
|
||||
const ai_components = searchParams.get('ai_components')
|
||||
|
||||
if (service_type) queryParams.set('service_type', service_type)
|
||||
if (criticality) queryParams.set('criticality', criticality)
|
||||
if (processes_pii) queryParams.set('processes_pii', processes_pii)
|
||||
if (ai_components) queryParams.set('ai_components', ai_components)
|
||||
|
||||
const url = `${BACKEND_URL}/api/v1/compliance/modules${queryParams.toString() ? `?${queryParams}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Compliance modules proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen', modules: [] },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
39
admin-v2/app/api/admin/compliance/modules/seed/route.ts
Normal file
39
admin-v2/app/api/admin/compliance/modules/seed/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Compliance Modules Seed API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/modules/seed`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(60000) // 60 seconds for seeding
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Modules seed proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
220
admin-v2/app/api/admin/compliance/regulations/route.ts
Normal file
220
admin-v2/app/api/admin/compliance/regulations/route.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Compliance Regulations API Route - Proxy to Backend
|
||||
*
|
||||
* Returns all 21 regulations with source URLs to original documents
|
||||
* Includes: GDPR, ePrivacy, TDDDG, SCC, DPF, AI Act, CRA, NIS2, EU CSA,
|
||||
* Data Act, DGA, DSA, EAA, DSM, PLD, GPSR, BSI-TR-03161 (1-3), BSI C5, DORA
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/regulations`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// If backend doesn't have this endpoint yet, return seed data
|
||||
if (response.status === 404) {
|
||||
return NextResponse.json({
|
||||
regulations: getStaticRegulations()
|
||||
})
|
||||
}
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Regulations proxy error:', error)
|
||||
// Return static data as fallback
|
||||
return NextResponse.json({
|
||||
regulations: getStaticRegulations()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Static seed data with source URLs - matches regulations.py
|
||||
function getStaticRegulations() {
|
||||
return [
|
||||
{
|
||||
id: '1', code: 'GDPR', name: 'DSGVO',
|
||||
full_name: 'Verordnung (EU) 2016/679 - Datenschutz-Grundverordnung',
|
||||
regulation_type: 'eu_regulation',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng',
|
||||
description: 'Grundverordnung zum Schutz natuerlicher Personen bei der Verarbeitung personenbezogener Daten.',
|
||||
is_active: true, requirement_count: 99,
|
||||
},
|
||||
{
|
||||
id: '2', code: 'EPRIVACY', name: 'ePrivacy-Richtlinie',
|
||||
full_name: 'Richtlinie 2002/58/EG',
|
||||
regulation_type: 'eu_directive',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/dir/2002/58/oj/eng',
|
||||
description: 'Datenschutz in der elektronischen Kommunikation, Cookies und Tracking.',
|
||||
is_active: true, requirement_count: 25,
|
||||
},
|
||||
{
|
||||
id: '3', code: 'TDDDG', name: 'TDDDG',
|
||||
full_name: 'Telekommunikation-Digitale-Dienste-Datenschutz-Gesetz',
|
||||
regulation_type: 'de_law',
|
||||
source_url: 'https://www.gesetze-im-internet.de/ttdsg/',
|
||||
description: 'Deutsche Umsetzung der ePrivacy-Richtlinie.',
|
||||
is_active: true, requirement_count: 15,
|
||||
},
|
||||
{
|
||||
id: '4', code: 'SCC', name: 'Standardvertragsklauseln',
|
||||
full_name: 'Durchfuehrungsbeschluss (EU) 2021/914',
|
||||
regulation_type: 'eu_regulation',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/dec_impl/2021/914/oj/eng',
|
||||
description: 'Standardvertragsklauseln fuer Drittlandtransfers.',
|
||||
is_active: true, requirement_count: 18,
|
||||
},
|
||||
{
|
||||
id: '5', code: 'DPF', name: 'EU-US Data Privacy Framework',
|
||||
full_name: 'Durchfuehrungsbeschluss (EU) 2023/1795',
|
||||
regulation_type: 'eu_regulation',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/dec_impl/2023/1795/oj',
|
||||
description: 'Angemessenheitsbeschluss fuer USA-Transfers.',
|
||||
is_active: true, requirement_count: 12,
|
||||
},
|
||||
{
|
||||
id: '6', code: 'AIACT', name: 'EU AI Act',
|
||||
full_name: 'Verordnung (EU) 2024/1689 - KI-Verordnung',
|
||||
regulation_type: 'eu_regulation',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/reg/2024/1689/oj/eng',
|
||||
description: 'EU-Verordnung zur Regulierung von KI-Systemen nach Risikostufen.',
|
||||
is_active: true, requirement_count: 85,
|
||||
},
|
||||
{
|
||||
id: '7', code: 'CRA', name: 'Cyber Resilience Act',
|
||||
full_name: 'Verordnung (EU) 2024/2847',
|
||||
regulation_type: 'eu_regulation',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng',
|
||||
description: 'Cybersicherheitsanforderungen, SBOM-Pflicht.',
|
||||
is_active: true, requirement_count: 45,
|
||||
},
|
||||
{
|
||||
id: '8', code: 'NIS2', name: 'NIS2-Richtlinie',
|
||||
full_name: 'Richtlinie (EU) 2022/2555',
|
||||
regulation_type: 'eu_directive',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/dir/2022/2555/oj/eng',
|
||||
description: 'Cybersicherheit fuer wesentliche Einrichtungen.',
|
||||
is_active: true, requirement_count: 46,
|
||||
},
|
||||
{
|
||||
id: '9', code: 'EUCSA', name: 'EU Cybersecurity Act',
|
||||
full_name: 'Verordnung (EU) 2019/881',
|
||||
regulation_type: 'eu_regulation',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/reg/2019/881/oj/eng',
|
||||
description: 'ENISA und Cybersicherheitszertifizierung.',
|
||||
is_active: true, requirement_count: 35,
|
||||
},
|
||||
{
|
||||
id: '10', code: 'DATAACT', name: 'Data Act',
|
||||
full_name: 'Verordnung (EU) 2023/2854',
|
||||
regulation_type: 'eu_regulation',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/reg/2023/2854/oj/eng',
|
||||
description: 'Fairer Datenzugang, IoT-Daten, Cloud-Wechsel.',
|
||||
is_active: true, requirement_count: 42,
|
||||
},
|
||||
{
|
||||
id: '11', code: 'DGA', name: 'Data Governance Act',
|
||||
full_name: 'Verordnung (EU) 2022/868',
|
||||
regulation_type: 'eu_regulation',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/reg/2022/868/oj/eng',
|
||||
description: 'Weiterverwendung oeffentlicher Daten.',
|
||||
is_active: true, requirement_count: 35,
|
||||
},
|
||||
{
|
||||
id: '12', code: 'DSA', name: 'Digital Services Act',
|
||||
full_name: 'Verordnung (EU) 2022/2065',
|
||||
regulation_type: 'eu_regulation',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/reg/2022/2065/oj/eng',
|
||||
description: 'Digitale Dienste, Transparenzpflichten.',
|
||||
is_active: true, requirement_count: 93,
|
||||
},
|
||||
{
|
||||
id: '13', code: 'EAA', name: 'European Accessibility Act',
|
||||
full_name: 'Richtlinie (EU) 2019/882',
|
||||
regulation_type: 'eu_directive',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/dir/2019/882/oj/eng',
|
||||
description: 'Barrierefreiheit digitaler Produkte.',
|
||||
is_active: true, requirement_count: 25,
|
||||
},
|
||||
{
|
||||
id: '14', code: 'DSM', name: 'DSM-Urheberrechtsrichtlinie',
|
||||
full_name: 'Richtlinie (EU) 2019/790',
|
||||
regulation_type: 'eu_directive',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/dir/2019/790/oj/eng',
|
||||
description: 'Urheberrecht, Text- und Data-Mining.',
|
||||
is_active: true, requirement_count: 22,
|
||||
},
|
||||
{
|
||||
id: '15', code: 'PLD', name: 'Produkthaftungsrichtlinie',
|
||||
full_name: 'Richtlinie (EU) 2024/2853',
|
||||
regulation_type: 'eu_directive',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/dir/2024/2853/oj/eng',
|
||||
description: 'Produkthaftung inkl. Software und KI.',
|
||||
is_active: true, requirement_count: 18,
|
||||
},
|
||||
{
|
||||
id: '16', code: 'GPSR', name: 'General Product Safety',
|
||||
full_name: 'Verordnung (EU) 2023/988',
|
||||
regulation_type: 'eu_regulation',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/reg/2023/988/oj/eng',
|
||||
description: 'Allgemeine Produktsicherheit.',
|
||||
is_active: true, requirement_count: 30,
|
||||
},
|
||||
{
|
||||
id: '17', code: 'BSI-TR-03161-1', name: 'BSI-TR-03161 Teil 1',
|
||||
full_name: 'BSI Technische Richtlinie - Allgemeine Anforderungen',
|
||||
regulation_type: 'bsi_standard',
|
||||
source_url: 'https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR03161/BSI-TR-03161-1.html',
|
||||
description: 'Allgemeine Sicherheitsanforderungen (45 Pruefaspekte).',
|
||||
is_active: true, requirement_count: 45,
|
||||
},
|
||||
{
|
||||
id: '18', code: 'BSI-TR-03161-2', name: 'BSI-TR-03161 Teil 2',
|
||||
full_name: 'BSI Technische Richtlinie - Web-Anwendungen',
|
||||
regulation_type: 'bsi_standard',
|
||||
source_url: 'https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR03161/BSI-TR-03161-2.html',
|
||||
description: 'Web-Sicherheit (40 Pruefaspekte).',
|
||||
is_active: true, requirement_count: 40,
|
||||
},
|
||||
{
|
||||
id: '19', code: 'BSI-TR-03161-3', name: 'BSI-TR-03161 Teil 3',
|
||||
full_name: 'BSI Technische Richtlinie - Hintergrundsysteme',
|
||||
regulation_type: 'bsi_standard',
|
||||
source_url: 'https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR03161/BSI-TR-03161-3.html',
|
||||
description: 'Backend-Sicherheit (35 Pruefaspekte).',
|
||||
is_active: true, requirement_count: 35,
|
||||
},
|
||||
{
|
||||
id: '20', code: 'BSI-C5', name: 'BSI C5',
|
||||
full_name: 'Cloud Computing Compliance Criteria Catalogue',
|
||||
regulation_type: 'bsi_standard',
|
||||
source_url: 'https://www.bsi.bund.de/DE/Themen/Unternehmen-und-Organisationen/Informationen-und-Empfehlungen/Empfehlungen-nach-Angriffszielen/Cloud-Computing/Kriterienkatalog-C5/kriterienkatalog-c5_node.html',
|
||||
description: 'Deutscher Cloud-Sicherheitsstandard mit 121 Kriterien in 17 Bereichen (OIS, SP, HR, AM, PS, OPS, COS, IDM, CRY, SIM, BCM, COM, SA, SUA, PI).',
|
||||
is_active: true, requirement_count: 121,
|
||||
},
|
||||
{
|
||||
id: '21', code: 'DORA', name: 'DORA',
|
||||
full_name: 'Verordnung (EU) 2022/2554 - Digital Operational Resilience Act',
|
||||
regulation_type: 'eu_regulation',
|
||||
source_url: 'https://eur-lex.europa.eu/eli/reg/2022/2554/oj/deu',
|
||||
description: 'EU-Verordnung fuer digitale operationale Resilienz im Finanzsektor. IKT-Risikomanagement, Incident-Reporting, Resilienztests, Drittparteienrisiko.',
|
||||
is_active: true, requirement_count: 64,
|
||||
},
|
||||
]
|
||||
}
|
||||
109
admin-v2/app/api/admin/compliance/requirements/route.ts
Normal file
109
admin-v2/app/api/admin/compliance/requirements/route.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Compliance Requirements API Route - Proxy to Backend
|
||||
*
|
||||
* Returns requirements for a specific regulation with implementation status
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const regulationCode = searchParams.get('regulation_code')
|
||||
|
||||
if (!regulationCode) {
|
||||
return NextResponse.json(
|
||||
{ error: 'regulation_code parameter required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Build query string for backend
|
||||
const params = new URLSearchParams()
|
||||
params.set('regulation_code', regulationCode)
|
||||
if (searchParams.get('status')) params.set('status', searchParams.get('status')!)
|
||||
if (searchParams.get('priority')) params.set('priority', searchParams.get('priority')!)
|
||||
if (searchParams.get('search')) params.set('search', searchParams.get('search')!)
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/compliance/requirements?${params}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: AbortSignal.timeout(30000)
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
// Return static BSI data as fallback if backend not available
|
||||
if (response.status === 404 && regulationCode.startsWith('BSI')) {
|
||||
return NextResponse.json({
|
||||
requirements: getBSIRequirements(regulationCode)
|
||||
})
|
||||
}
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Requirements proxy error:', error)
|
||||
// Return fallback data for BSI
|
||||
const regulationCode = request.nextUrl.searchParams.get('regulation_code')
|
||||
if (regulationCode?.startsWith('BSI')) {
|
||||
return NextResponse.json({
|
||||
requirements: getBSIRequirements(regulationCode)
|
||||
})
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen', requirements: [] },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Static BSI requirements as fallback (subset)
|
||||
function getBSIRequirements(code: string) {
|
||||
if (code === 'BSI-TR-03161-1') {
|
||||
return [
|
||||
{ id: '1', regulation_code: code, article: 'O.Purp_1', title: 'Zweckbindung', description: 'Anwendungszweck klar definiert', implementation_status: 'implemented', priority: 1, controls_count: 2 },
|
||||
{ id: '2', regulation_code: code, article: 'O.Data_1', title: 'Datenminimierung', description: 'Nur notwendige Daten erheben', implementation_status: 'implemented', priority: 1, controls_count: 3 },
|
||||
{ id: '3', regulation_code: code, article: 'O.Auth_1', title: 'Authentifizierung', description: 'Sichere Authentifizierungsmechanismen', implementation_status: 'verified', priority: 1, controls_count: 4 },
|
||||
{ id: '4', regulation_code: code, article: 'O.Auth_2', title: 'Passwortrichtlinie', description: 'Starke Passwoerter erzwingen', implementation_status: 'implemented', priority: 1, controls_count: 2 },
|
||||
{ id: '5', regulation_code: code, article: 'O.Cryp_1', title: 'TLS-Verschluesselung', description: 'TLS 1.2+ fuer Transport', implementation_status: 'verified', priority: 1, controls_count: 2 },
|
||||
{ id: '6', regulation_code: code, article: 'O.Cryp_2', title: 'Encryption at Rest', description: 'Sensible Daten verschluesseln', implementation_status: 'implemented', priority: 1, controls_count: 2 },
|
||||
{ id: '7', regulation_code: code, article: 'O.Priv_1', title: 'Datenschutzerklaerung', description: 'Transparente Information', implementation_status: 'verified', priority: 1, controls_count: 1 },
|
||||
{ id: '8', regulation_code: code, article: 'O.Log_1', title: 'Security Logging', description: 'Sicherheitsereignisse protokollieren', implementation_status: 'in_progress', priority: 1, controls_count: 2 },
|
||||
]
|
||||
}
|
||||
if (code === 'BSI-TR-03161-2') {
|
||||
return [
|
||||
{ id: '20', regulation_code: code, article: 'O.Sess_1', title: 'Session-Timeout', description: 'Automatische Sitzungsbeendigung', implementation_status: 'implemented', priority: 1, controls_count: 2 },
|
||||
{ id: '21', regulation_code: code, article: 'O.Input_1', title: 'Eingabevalidierung', description: 'Alle Eingaben validieren', implementation_status: 'verified', priority: 1, controls_count: 3 },
|
||||
{ id: '22', regulation_code: code, article: 'O.SQL_1', title: 'SQL-Injection Schutz', description: 'Prepared Statements', implementation_status: 'verified', priority: 1, controls_count: 2 },
|
||||
{ id: '23', regulation_code: code, article: 'O.XSS_1', title: 'XSS-Schutz', description: 'Output Encoding', implementation_status: 'verified', priority: 1, controls_count: 3 },
|
||||
{ id: '24', regulation_code: code, article: 'O.CSRF_1', title: 'CSRF-Schutz', description: 'Anti-CSRF Token', implementation_status: 'implemented', priority: 1, controls_count: 2 },
|
||||
{ id: '25', regulation_code: code, article: 'O.Head_1', title: 'Security Headers', description: 'X-Content-Type-Options', implementation_status: 'verified', priority: 1, controls_count: 1 },
|
||||
{ id: '26', regulation_code: code, article: 'O.API_1', title: 'API-Authentifizierung', description: 'JWT/OAuth', implementation_status: 'verified', priority: 1, controls_count: 2 },
|
||||
{ id: '27', regulation_code: code, article: 'O.API_2', title: 'Rate Limiting', description: 'Anfragen begrenzen', implementation_status: 'implemented', priority: 1, controls_count: 1 },
|
||||
]
|
||||
}
|
||||
if (code === 'BSI-TR-03161-3') {
|
||||
return [
|
||||
{ id: '40', regulation_code: code, article: 'O.Arch_1', title: 'Defense in Depth', description: 'Mehrschichtige Sicherheit', implementation_status: 'implemented', priority: 1, controls_count: 3 },
|
||||
{ id: '41', regulation_code: code, article: 'O.DB_1', title: 'Datenbank-Sicherheit', description: 'DB abhaerten', implementation_status: 'implemented', priority: 1, controls_count: 2 },
|
||||
{ id: '42', regulation_code: code, article: 'O.Cont_1', title: 'Container-Sicherheit', description: 'Images scannen', implementation_status: 'in_progress', priority: 1, controls_count: 2 },
|
||||
{ id: '43', regulation_code: code, article: 'O.Sec_1', title: 'Secrets Management', description: 'Zentrale Secrets-Verwaltung', implementation_status: 'verified', priority: 1, controls_count: 2 },
|
||||
{ id: '44', regulation_code: code, article: 'O.Mon_1', title: 'Zentrale Logs', description: 'Log-Aggregation', implementation_status: 'implemented', priority: 1, controls_count: 1 },
|
||||
{ id: '45', regulation_code: code, article: 'O.CI_1', title: 'Pipeline-Sicherheit', description: 'CI/CD absichern', implementation_status: 'in_progress', priority: 1, controls_count: 2 },
|
||||
{ id: '46', regulation_code: code, article: 'O.DR_1', title: 'Backup-Strategie', description: '3-2-1 Backup-Regel', implementation_status: 'implemented', priority: 1, controls_count: 1 },
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
83
admin-v2/app/api/admin/compliance/risks/[riskId]/route.ts
Normal file
83
admin-v2/app/api/admin/compliance/risks/[riskId]/route.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Risk Update API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ riskId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { riskId } = await params
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/compliance/risks/${riskId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Risk update proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ riskId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { riskId } = await params
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/compliance/risks/${riskId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Get risk proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
81
admin-v2/app/api/admin/compliance/risks/route.ts
Normal file
81
admin-v2/app/api/admin/compliance/risks/route.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Compliance Risks API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
const level = searchParams.get('level')
|
||||
const status = searchParams.get('status')
|
||||
const category = searchParams.get('category')
|
||||
|
||||
if (level) queryParams.set('level', level)
|
||||
if (status) queryParams.set('status', status)
|
||||
if (category) queryParams.set('category', category)
|
||||
|
||||
const url = `${BACKEND_URL}/api/v1/compliance/risks${queryParams.toString() ? `?${queryParams}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Compliance risks proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen', risks: [] },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/risks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Create risk proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
39
admin-v2/app/api/admin/compliance/seed/route.ts
Normal file
39
admin-v2/app/api/admin/compliance/seed/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Compliance Seed API Route - Proxy to Backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/seed`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(60000) // 60s timeout for seeding
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Compliance seed proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
48
admin-v2/app/api/admin/consent/audit-log/route.ts
Normal file
48
admin-v2/app/api/admin/consent/audit-log/route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Consent Admin API Route - Audit Log Proxy
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const CONSENT_SERVICE_URL = process.env.CONSENT_SERVICE_URL || 'http://localhost:8081'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
const limit = request.nextUrl.searchParams.get('limit') || '100'
|
||||
const offset = request.nextUrl.searchParams.get('offset') || '0'
|
||||
const action = request.nextUrl.searchParams.get('action') || ''
|
||||
|
||||
const url = new URL(`${CONSENT_SERVICE_URL}/api/v1/internal/audit-log`)
|
||||
url.searchParams.set('limit', limit)
|
||||
url.searchParams.set('offset', offset)
|
||||
if (action) {
|
||||
url.searchParams.set('action', action)
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
signal: AbortSignal.timeout(10000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Consent Service Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Audit log proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Consent Service fehlgeschlagen', entries: [] },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
40
admin-v2/app/api/admin/consent/deadlines/route.ts
Normal file
40
admin-v2/app/api/admin/consent/deadlines/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Consent Admin API Route - Deadlines Proxy
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const CONSENT_SERVICE_URL = process.env.CONSENT_SERVICE_URL || 'http://localhost:8081'
|
||||
|
||||
// Trigger deadline processing
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
|
||||
const response = await fetch(`${CONSENT_SERVICE_URL}/api/v1/admin/deadlines/process`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Consent Service Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Deadlines process proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Consent Service fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Consent Admin API Route - Document Versions Proxy
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const CONSENT_SERVICE_URL = process.env.CONSENT_SERVICE_URL || 'http://localhost:8081'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
|
||||
const response = await fetch(`${CONSENT_SERVICE_URL}/api/v1/documents/${id}/versions`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
signal: AbortSignal.timeout(10000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Consent Service Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Versions proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Consent Service fehlgeschlagen', versions: [] },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${CONSENT_SERVICE_URL}/api/v1/documents/${id}/versions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(10000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Consent Service Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create version proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Consent Service fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
75
admin-v2/app/api/admin/consent/documents/route.ts
Normal file
75
admin-v2/app/api/admin/consent/documents/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Consent Admin API Route - Documents Proxy
|
||||
*
|
||||
* Proxies requests to consent service documents endpoints
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const CONSENT_SERVICE_URL = process.env.CONSENT_SERVICE_URL || 'http://localhost:8081'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
|
||||
const response = await fetch(`${CONSENT_SERVICE_URL}/api/v1/internal/documents`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
signal: AbortSignal.timeout(10000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('Consent service documents error:', response.status, errorText)
|
||||
return NextResponse.json(
|
||||
{ error: `Consent Service Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Documents proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Consent Service fehlgeschlagen', documents: [] },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${CONSENT_SERVICE_URL}/api/v1/documents`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(10000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Consent Service Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create document proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Consent Service fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
44
admin-v2/app/api/admin/consent/stats/route.ts
Normal file
44
admin-v2/app/api/admin/consent/stats/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Consent Admin API Route - Stats Proxy
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const CONSENT_SERVICE_URL = process.env.CONSENT_SERVICE_URL || 'http://localhost:8081'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
const documentType = request.nextUrl.searchParams.get('document_type') || ''
|
||||
|
||||
const url = new URL(`${CONSENT_SERVICE_URL}/api/v1/internal/stats/consents`)
|
||||
if (documentType) {
|
||||
url.searchParams.set('document_type', documentType)
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(authHeader ? { 'Authorization': authHeader } : {})
|
||||
},
|
||||
signal: AbortSignal.timeout(10000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Consent Service Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Stats proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Consent Service fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Version Approval History API Route
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ versionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { versionId } = await params
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/consent/admin/versions/${versionId}/approval-history`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...(request.headers.get('Authorization')
|
||||
? { Authorization: request.headers.get('Authorization')! }
|
||||
: {}),
|
||||
},
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Approval history proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Approve Version API Route
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ versionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { versionId } = await params
|
||||
const body = await request.json().catch(() => ({}))
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/consent/admin/versions/${versionId}/approve`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('Authorization')
|
||||
? { Authorization: request.headers.get('Authorization')! }
|
||||
: {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Approve proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Publish Version API Route
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ versionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { versionId } = await params
|
||||
const body = await request.json().catch(() => ({}))
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/consent/admin/versions/${versionId}/publish`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('Authorization')
|
||||
? { Authorization: request.headers.get('Authorization')! }
|
||||
: {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Publish proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Reject Version API Route
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ versionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { versionId } = await params
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/consent/admin/versions/${versionId}/reject`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('Authorization')
|
||||
? { Authorization: request.headers.get('Authorization')! }
|
||||
: {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Reject proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
87
admin-v2/app/api/admin/consent/versions/[versionId]/route.ts
Normal file
87
admin-v2/app/api/admin/consent/versions/[versionId]/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Consent Version Detail API Route - Update/Delete version
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ versionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { versionId } = await params
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/consent/admin/versions/${versionId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('Authorization')
|
||||
? { Authorization: request.headers.get('Authorization')! }
|
||||
: {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Version update proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ versionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { versionId } = await params
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/consent/admin/versions/${versionId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
...(request.headers.get('Authorization')
|
||||
? { Authorization: request.headers.get('Authorization')! }
|
||||
: {}),
|
||||
},
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Version delete proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Submit Version for Review API Route
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ versionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { versionId } = await params
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/consent/admin/versions/${versionId}/submit-review`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('Authorization')
|
||||
? { Authorization: request.headers.get('Authorization')! }
|
||||
: {}),
|
||||
},
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Submit review proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
42
admin-v2/app/api/admin/consent/versions/route.ts
Normal file
42
admin-v2/app/api/admin/consent/versions/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Consent Versions API Route - Create new version
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/consent/admin/versions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('Authorization')
|
||||
? { Authorization: request.headers.get('Authorization')! }
|
||||
: {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Version create proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
173
admin-v2/app/api/admin/health/route.ts
Normal file
173
admin-v2/app/api/admin/health/route.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* Server-side health check proxy
|
||||
* Checks all services via HTTP from the server to avoid mixed-content issues
|
||||
*/
|
||||
|
||||
interface ServiceConfig {
|
||||
name: string
|
||||
port: number
|
||||
endpoint: string
|
||||
category: 'core' | 'ai' | 'database' | 'storage'
|
||||
}
|
||||
|
||||
const SERVICES: ServiceConfig[] = [
|
||||
// Core Services
|
||||
{ name: 'Backend API', port: 8000, endpoint: '/health', category: 'core' },
|
||||
{ name: 'Consent Service', port: 8081, endpoint: '/api/v1/health', category: 'core' },
|
||||
{ name: 'Voice Service', port: 8091, endpoint: '/health', category: 'core' },
|
||||
{ name: 'Klausur Service', port: 8086, endpoint: '/health', category: 'core' },
|
||||
{ name: 'Mail Service (Mailpit)', port: 8025, endpoint: '/api/v1/info', category: 'core' },
|
||||
{ name: 'Edu Search', port: 8088, endpoint: '/health', category: 'core' },
|
||||
{ name: 'H5P Service', port: 8092, endpoint: '/health', category: 'core' },
|
||||
|
||||
// AI Services
|
||||
{ name: 'Ollama/LLM', port: 11434, endpoint: '/api/tags', category: 'ai' },
|
||||
{ name: 'Embedding Service', port: 8087, endpoint: '/health', category: 'ai' },
|
||||
|
||||
// Databases - checked via backend proxy
|
||||
{ name: 'PostgreSQL', port: 5432, endpoint: '', category: 'database' },
|
||||
{ name: 'Qdrant (Vector DB)', port: 6333, endpoint: '/collections', category: 'database' },
|
||||
{ name: 'Valkey (Cache)', port: 6379, endpoint: '', category: 'database' },
|
||||
|
||||
// Storage
|
||||
{ name: 'MinIO (S3)', port: 9000, endpoint: '/minio/health/live', category: 'storage' },
|
||||
]
|
||||
|
||||
// Use internal Docker hostnames when running in container
|
||||
const getInternalHost = (port: number): string => {
|
||||
// Map ports to internal Docker service names
|
||||
const serviceMap: Record<number, string> = {
|
||||
8000: 'backend',
|
||||
8081: 'consent-service',
|
||||
8091: 'voice-service',
|
||||
8086: 'klausur-service',
|
||||
8025: 'mailpit',
|
||||
8088: 'edu-search-service',
|
||||
8092: 'h5p-service',
|
||||
11434: 'ollama',
|
||||
8087: 'embedding-service',
|
||||
5432: 'postgres',
|
||||
6333: 'qdrant',
|
||||
6379: 'valkey',
|
||||
9000: 'minio',
|
||||
}
|
||||
|
||||
// In container, use Docker hostnames; otherwise use localhost
|
||||
const host = process.env.BACKEND_URL ? serviceMap[port] || 'localhost' : 'localhost'
|
||||
return host
|
||||
}
|
||||
|
||||
async function checkService(service: ServiceConfig): Promise<{
|
||||
name: string
|
||||
port: number
|
||||
category: string
|
||||
status: 'online' | 'offline' | 'degraded'
|
||||
responseTime: number
|
||||
details?: string
|
||||
}> {
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
// Special handling for PostgreSQL - check via backend
|
||||
if (service.port === 5432) {
|
||||
const backendHost = getInternalHost(8000)
|
||||
const response = await fetch(`http://${backendHost}:8000/api/tests/db-status`, {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(3000),
|
||||
})
|
||||
const responseTime = Date.now() - startTime
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
return {
|
||||
...service,
|
||||
status: 'online',
|
||||
responseTime,
|
||||
details: data.host || undefined
|
||||
}
|
||||
}
|
||||
return { ...service, status: 'offline', responseTime }
|
||||
}
|
||||
|
||||
// Special handling for Valkey - check via backend
|
||||
if (service.port === 6379) {
|
||||
const backendHost = getInternalHost(8000)
|
||||
try {
|
||||
const response = await fetch(`http://${backendHost}:8000/api/tests/cache-status`, {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(3000),
|
||||
})
|
||||
const responseTime = Date.now() - startTime
|
||||
|
||||
if (response.ok) {
|
||||
return { ...service, status: 'online', responseTime }
|
||||
}
|
||||
} catch {
|
||||
// Fallback: assume online if backend is reachable (Valkey is usually bundled)
|
||||
}
|
||||
const responseTime = Date.now() - startTime
|
||||
return { ...service, status: 'online', responseTime, details: 'via Backend' }
|
||||
}
|
||||
|
||||
const host = getInternalHost(service.port)
|
||||
const url = `http://${host}:${service.port}${service.endpoint}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
|
||||
const responseTime = Date.now() - startTime
|
||||
|
||||
if (response.ok) {
|
||||
// Special handling for Ollama
|
||||
if (service.port === 11434) {
|
||||
try {
|
||||
const data = await response.json()
|
||||
const modelCount = data.models?.length || 0
|
||||
return {
|
||||
...service,
|
||||
status: 'online',
|
||||
responseTime,
|
||||
details: `${modelCount} Modell${modelCount !== 1 ? 'e' : ''} geladen`
|
||||
}
|
||||
} catch {
|
||||
return { ...service, status: 'online', responseTime }
|
||||
}
|
||||
}
|
||||
return { ...service, status: 'online', responseTime }
|
||||
} else if (response.status >= 500) {
|
||||
return { ...service, status: 'degraded', responseTime, details: `HTTP ${response.status}` }
|
||||
} else {
|
||||
return { ...service, status: 'offline', responseTime }
|
||||
}
|
||||
} catch (error) {
|
||||
const responseTime = Date.now() - startTime
|
||||
return {
|
||||
...service,
|
||||
status: 'offline',
|
||||
responseTime,
|
||||
details: error instanceof Error ? error.message : 'Verbindungsfehler'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const results = await Promise.all(SERVICES.map(checkService))
|
||||
|
||||
return NextResponse.json({
|
||||
services: results,
|
||||
timestamp: new Date().toISOString(),
|
||||
onlineCount: results.filter(s => s.status === 'online').length,
|
||||
totalCount: results.length
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to check services', details: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
338
admin-v2/app/api/admin/infrastructure/mac-mini/route.ts
Normal file
338
admin-v2/app/api/admin/infrastructure/mac-mini/route.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* Mac Mini System Monitoring API
|
||||
*
|
||||
* Provides system stats and Docker container management
|
||||
* Requires Docker socket mounted at /var/run/docker.sock
|
||||
*/
|
||||
|
||||
interface ContainerInfo {
|
||||
id: string
|
||||
name: string
|
||||
image: string
|
||||
status: string
|
||||
state: string
|
||||
created: string
|
||||
ports: string[]
|
||||
cpu_percent: number
|
||||
memory_usage: string
|
||||
memory_limit: string
|
||||
memory_percent: number
|
||||
network_rx: string
|
||||
network_tx: string
|
||||
}
|
||||
|
||||
interface SystemStats {
|
||||
hostname: string
|
||||
platform: string
|
||||
arch: string
|
||||
uptime: number
|
||||
cpu: {
|
||||
model: string
|
||||
cores: number
|
||||
usage_percent: number
|
||||
}
|
||||
memory: {
|
||||
total: string
|
||||
used: string
|
||||
free: string
|
||||
usage_percent: number
|
||||
}
|
||||
disk: {
|
||||
total: string
|
||||
used: string
|
||||
free: string
|
||||
usage_percent: number
|
||||
}
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
interface DockerStats {
|
||||
containers: ContainerInfo[]
|
||||
total_containers: number
|
||||
running_containers: number
|
||||
stopped_containers: number
|
||||
}
|
||||
|
||||
// Helper to format bytes
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// Helper to format uptime
|
||||
function formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400)
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
if (days > 0) return `${days}d ${hours}h ${minutes}m`
|
||||
if (hours > 0) return `${hours}h ${minutes}m`
|
||||
return `${minutes}m`
|
||||
}
|
||||
|
||||
// Get Docker stats via socket
|
||||
async function getDockerStats(): Promise<DockerStats> {
|
||||
const DOCKER_SOCKET = process.env.DOCKER_HOST || 'unix:///var/run/docker.sock'
|
||||
|
||||
try {
|
||||
// Fetch container list
|
||||
const containersResponse = await fetch(`${DOCKER_SOCKET.replace('unix://', 'http://localhost')}/containers/json?all=true`, {
|
||||
// @ts-expect-error - Node.js fetch supports unix sockets via socketPath
|
||||
socketPath: '/var/run/docker.sock',
|
||||
})
|
||||
|
||||
if (!containersResponse.ok) {
|
||||
throw new Error('Failed to fetch containers')
|
||||
}
|
||||
|
||||
const containers = await containersResponse.json()
|
||||
|
||||
// Get stats for running containers
|
||||
const containerInfos: ContainerInfo[] = await Promise.all(
|
||||
containers.map(async (container: Record<string, unknown>) => {
|
||||
const names = container.Names as string[]
|
||||
const name = names?.[0]?.replace(/^\//, '') || 'unknown'
|
||||
const state = container.State as string
|
||||
const status = container.Status as string
|
||||
const image = container.Image as string
|
||||
const created = container.Created as number
|
||||
const ports = container.Ports as Array<{ PrivatePort: number; PublicPort?: number; Type: string }>
|
||||
|
||||
let cpu_percent = 0
|
||||
let memory_usage = '0 B'
|
||||
let memory_limit = '0 B'
|
||||
let memory_percent = 0
|
||||
let network_rx = '0 B'
|
||||
let network_tx = '0 B'
|
||||
|
||||
// Get live stats for running containers
|
||||
if (state === 'running') {
|
||||
try {
|
||||
const statsResponse = await fetch(
|
||||
`http://localhost/containers/${container.Id}/stats?stream=false`,
|
||||
{
|
||||
// @ts-expect-error - Node.js fetch supports unix sockets
|
||||
socketPath: '/var/run/docker.sock',
|
||||
}
|
||||
)
|
||||
|
||||
if (statsResponse.ok) {
|
||||
const stats = await statsResponse.json()
|
||||
|
||||
// Calculate CPU usage
|
||||
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage -
|
||||
(stats.precpu_stats?.cpu_usage?.total_usage || 0)
|
||||
const systemDelta = stats.cpu_stats.system_cpu_usage -
|
||||
(stats.precpu_stats?.system_cpu_usage || 0)
|
||||
const cpuCount = stats.cpu_stats.online_cpus || 1
|
||||
|
||||
if (systemDelta > 0 && cpuDelta > 0) {
|
||||
cpu_percent = (cpuDelta / systemDelta) * cpuCount * 100
|
||||
}
|
||||
|
||||
// Memory usage
|
||||
const memUsage = stats.memory_stats?.usage || 0
|
||||
const memLimit = stats.memory_stats?.limit || 0
|
||||
memory_usage = formatBytes(memUsage)
|
||||
memory_limit = formatBytes(memLimit)
|
||||
memory_percent = memLimit > 0 ? (memUsage / memLimit) * 100 : 0
|
||||
|
||||
// Network stats
|
||||
const networks = stats.networks || {}
|
||||
let rxBytes = 0
|
||||
let txBytes = 0
|
||||
Object.values(networks).forEach((net: unknown) => {
|
||||
const network = net as { rx_bytes?: number; tx_bytes?: number }
|
||||
rxBytes += network.rx_bytes || 0
|
||||
txBytes += network.tx_bytes || 0
|
||||
})
|
||||
network_rx = formatBytes(rxBytes)
|
||||
network_tx = formatBytes(txBytes)
|
||||
}
|
||||
} catch {
|
||||
// Stats not available, use defaults
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: (container.Id as string).substring(0, 12),
|
||||
name,
|
||||
image: (image as string).split(':')[0].split('/').pop() || image,
|
||||
status,
|
||||
state,
|
||||
created: new Date(created * 1000).toISOString(),
|
||||
ports: ports?.map(p =>
|
||||
p.PublicPort ? `${p.PublicPort}:${p.PrivatePort}/${p.Type}` : `${p.PrivatePort}/${p.Type}`
|
||||
) || [],
|
||||
cpu_percent: Math.round(cpu_percent * 100) / 100,
|
||||
memory_usage,
|
||||
memory_limit,
|
||||
memory_percent: Math.round(memory_percent * 100) / 100,
|
||||
network_rx,
|
||||
network_tx,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Sort by name
|
||||
containerInfos.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
return {
|
||||
containers: containerInfos,
|
||||
total_containers: containerInfos.length,
|
||||
running_containers: containerInfos.filter(c => c.state === 'running').length,
|
||||
stopped_containers: containerInfos.filter(c => c.state !== 'running').length,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Docker stats error:', error)
|
||||
// Return empty stats if Docker socket not available
|
||||
return {
|
||||
containers: [],
|
||||
total_containers: 0,
|
||||
running_containers: 0,
|
||||
stopped_containers: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get system stats
|
||||
async function getSystemStats(): Promise<SystemStats> {
|
||||
const os = await import('os')
|
||||
|
||||
const cpus = os.cpus()
|
||||
const totalMem = os.totalmem()
|
||||
const freeMem = os.freemem()
|
||||
const usedMem = totalMem - freeMem
|
||||
|
||||
// Calculate CPU usage from cpus
|
||||
let totalIdle = 0
|
||||
let totalTick = 0
|
||||
cpus.forEach(cpu => {
|
||||
for (const type in cpu.times) {
|
||||
totalTick += cpu.times[type as keyof typeof cpu.times]
|
||||
}
|
||||
totalIdle += cpu.times.idle
|
||||
})
|
||||
const cpuUsage = 100 - (totalIdle / totalTick * 100)
|
||||
|
||||
// Disk stats (root partition)
|
||||
let diskTotal = 0
|
||||
let diskUsed = 0
|
||||
let diskFree = 0
|
||||
|
||||
try {
|
||||
const { execSync } = await import('child_process')
|
||||
const dfOutput = execSync('df -k / | tail -1').toString()
|
||||
const parts = dfOutput.split(/\s+/)
|
||||
diskTotal = parseInt(parts[1]) * 1024
|
||||
diskUsed = parseInt(parts[2]) * 1024
|
||||
diskFree = parseInt(parts[3]) * 1024
|
||||
} catch {
|
||||
// Disk stats not available
|
||||
}
|
||||
|
||||
return {
|
||||
hostname: os.hostname(),
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
uptime: os.uptime(),
|
||||
cpu: {
|
||||
model: cpus[0]?.model || 'Unknown',
|
||||
cores: cpus.length,
|
||||
usage_percent: Math.round(cpuUsage * 100) / 100,
|
||||
},
|
||||
memory: {
|
||||
total: formatBytes(totalMem),
|
||||
used: formatBytes(usedMem),
|
||||
free: formatBytes(freeMem),
|
||||
usage_percent: Math.round((usedMem / totalMem) * 100 * 100) / 100,
|
||||
},
|
||||
disk: {
|
||||
total: formatBytes(diskTotal),
|
||||
used: formatBytes(diskUsed),
|
||||
free: formatBytes(diskFree),
|
||||
usage_percent: diskTotal > 0 ? Math.round((diskUsed / diskTotal) * 100 * 100) / 100 : 0,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
// Container action (start/stop/restart)
|
||||
async function containerAction(containerId: string, action: 'start' | 'stop' | 'restart'): Promise<void> {
|
||||
const response = await fetch(
|
||||
`http://localhost/containers/${containerId}/${action}`,
|
||||
{
|
||||
method: 'POST',
|
||||
// @ts-expect-error - Node.js fetch supports unix sockets
|
||||
socketPath: '/var/run/docker.sock',
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok && response.status !== 304) {
|
||||
const error = await response.text()
|
||||
throw new Error(`Failed to ${action} container: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// GET - Fetch system and Docker stats
|
||||
export async function GET() {
|
||||
try {
|
||||
const [system, docker] = await Promise.all([
|
||||
getSystemStats(),
|
||||
getDockerStats(),
|
||||
])
|
||||
|
||||
return NextResponse.json({
|
||||
system,
|
||||
docker,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Mac Mini stats error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Container actions (start/stop/restart)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { container_id, action } = body
|
||||
|
||||
if (!container_id || !action) {
|
||||
return NextResponse.json(
|
||||
{ error: 'container_id and action required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!['start', 'stop', 'restart'].includes(action)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid action. Use: start, stop, restart' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
await containerAction(container_id, action)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Container ${action} successful`,
|
||||
container_id,
|
||||
action,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Container action error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Action failed' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
208
admin-v2/app/api/admin/infrastructure/woodpecker/route.ts
Normal file
208
admin-v2/app/api/admin/infrastructure/woodpecker/route.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
// Woodpecker API configuration
|
||||
const WOODPECKER_URL = process.env.WOODPECKER_URL || 'http://woodpecker-server:8000'
|
||||
const WOODPECKER_TOKEN = process.env.WOODPECKER_TOKEN || ''
|
||||
|
||||
export interface PipelineStep {
|
||||
name: string
|
||||
state: 'pending' | 'running' | 'success' | 'failure' | 'skipped'
|
||||
exit_code: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface Pipeline {
|
||||
id: number
|
||||
number: number
|
||||
status: 'pending' | 'running' | 'success' | 'failure' | 'error'
|
||||
event: string
|
||||
branch: string
|
||||
commit: string
|
||||
message: string
|
||||
author: string
|
||||
created: number
|
||||
started: number
|
||||
finished: number
|
||||
steps: PipelineStep[]
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
export interface WoodpeckerStatusResponse {
|
||||
status: 'online' | 'offline'
|
||||
pipelines: Pipeline[]
|
||||
lastUpdate: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const repoId = searchParams.get('repo') || '1'
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
|
||||
try {
|
||||
// Fetch pipelines from Woodpecker API
|
||||
const response = await fetch(
|
||||
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines?per_page=${limit}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json({
|
||||
status: 'offline',
|
||||
pipelines: [],
|
||||
lastUpdate: new Date().toISOString(),
|
||||
error: `Woodpecker API nicht erreichbar (${response.status})`
|
||||
} as WoodpeckerStatusResponse)
|
||||
}
|
||||
|
||||
const rawPipelines = await response.json()
|
||||
|
||||
// Transform pipelines to our format
|
||||
const pipelines: Pipeline[] = rawPipelines.map((p: any) => {
|
||||
// Extract errors from workflows/steps
|
||||
const errors: string[] = []
|
||||
const steps: PipelineStep[] = []
|
||||
|
||||
if (p.workflows) {
|
||||
for (const workflow of p.workflows) {
|
||||
if (workflow.children) {
|
||||
for (const child of workflow.children) {
|
||||
steps.push({
|
||||
name: child.name,
|
||||
state: child.state,
|
||||
exit_code: child.exit_code,
|
||||
error: child.error
|
||||
})
|
||||
if (child.state === 'failure' && child.error) {
|
||||
errors.push(`${child.name}: ${child.error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
number: p.number,
|
||||
status: p.status,
|
||||
event: p.event,
|
||||
branch: p.branch,
|
||||
commit: p.commit?.substring(0, 7) || '',
|
||||
message: p.message || '',
|
||||
author: p.author,
|
||||
created: p.created,
|
||||
started: p.started,
|
||||
finished: p.finished,
|
||||
steps,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
status: 'online',
|
||||
pipelines,
|
||||
lastUpdate: new Date().toISOString()
|
||||
} as WoodpeckerStatusResponse)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Woodpecker API error:', error)
|
||||
return NextResponse.json({
|
||||
status: 'offline',
|
||||
pipelines: [],
|
||||
lastUpdate: new Date().toISOString(),
|
||||
error: 'Fehler beim Abrufen des Woodpecker Status'
|
||||
} as WoodpeckerStatusResponse)
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger a new pipeline
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { repoId = '1', branch = 'main' } = body
|
||||
|
||||
const response = await fetch(
|
||||
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ branch }),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Pipeline konnte nicht gestartet werden' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
const pipeline = await response.json()
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
pipeline: {
|
||||
id: pipeline.id,
|
||||
number: pipeline.number,
|
||||
status: pipeline.status
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Pipeline trigger error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Fehler beim Starten der Pipeline' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Get pipeline logs
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { repoId = '1', pipelineNumber, stepId } = body
|
||||
|
||||
if (!pipelineNumber || !stepId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'pipelineNumber und stepId erforderlich' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines/${pipelineNumber}/logs/${stepId}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Logs nicht verfuegbar' },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const logs = await response.json()
|
||||
return NextResponse.json({ logs })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Pipeline logs error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Fehler beim Abrufen der Logs' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
81
admin-v2/app/api/admin/mail/route.ts
Normal file
81
admin-v2/app/api/admin/mail/route.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* Server-side proxy for Mailpit API
|
||||
* Avoids CORS and mixed-content issues by fetching from server
|
||||
*/
|
||||
|
||||
// Use internal Docker hostname when running in container
|
||||
const getMailpitHost = (): string => {
|
||||
return process.env.BACKEND_URL ? 'mailpit' : 'localhost'
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const host = getMailpitHost()
|
||||
const mailpitUrl = `http://${host}:8025/api/v1/info`
|
||||
|
||||
try {
|
||||
const response = await fetch(mailpitUrl, {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Mailpit API error', status: response.status },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Transform Mailpit response to our expected format
|
||||
return NextResponse.json({
|
||||
stats: {
|
||||
totalAccounts: 1,
|
||||
activeAccounts: 1,
|
||||
totalEmails: data.Messages || 0,
|
||||
unreadEmails: data.Unread || 0,
|
||||
totalTasks: 0,
|
||||
pendingTasks: 0,
|
||||
overdueTasks: 0,
|
||||
aiAnalyzedCount: 0,
|
||||
lastSyncTime: new Date().toISOString(),
|
||||
},
|
||||
accounts: [{
|
||||
id: 'mailpit-dev',
|
||||
email: 'dev@mailpit.local',
|
||||
displayName: 'Mailpit (Development)',
|
||||
imapHost: 'mailpit',
|
||||
imapPort: 1143,
|
||||
smtpHost: 'mailpit',
|
||||
smtpPort: 1025,
|
||||
status: 'active' as const,
|
||||
lastSync: new Date().toISOString(),
|
||||
emailCount: data.Messages || 0,
|
||||
unreadCount: data.Unread || 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
}],
|
||||
syncStatus: {
|
||||
running: false,
|
||||
accountsInProgress: [],
|
||||
lastCompleted: new Date().toISOString(),
|
||||
errors: [],
|
||||
},
|
||||
mailpitInfo: {
|
||||
version: data.Version,
|
||||
databaseSize: data.DatabaseSize,
|
||||
uptime: data.RuntimeStats?.Uptime,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch from Mailpit:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to connect to Mailpit',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
172
admin-v2/app/api/admin/middleware/[...path]/route.ts
Normal file
172
admin-v2/app/api/admin/middleware/[...path]/route.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Middleware Admin API Proxy - Catch-all route
|
||||
* Proxies all /api/admin/middleware/* requests to backend
|
||||
* Forwards authentication cookies for session-based auth
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
function getForwardHeaders(request: NextRequest): HeadersInit {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
// Forward cookie for session auth
|
||||
const cookie = request.headers.get('cookie')
|
||||
if (cookie) {
|
||||
headers['Cookie'] = cookie
|
||||
}
|
||||
|
||||
// Forward authorization header if present
|
||||
const auth = request.headers.get('authorization')
|
||||
if (auth) {
|
||||
headers['Authorization'] = auth
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
const pathStr = path.join('/')
|
||||
const searchParams = request.nextUrl.searchParams.toString()
|
||||
const url = `${BACKEND_URL}/api/admin/middleware/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getForwardHeaders(request),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Middleware API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
const pathStr = path.join('/')
|
||||
const url = `${BACKEND_URL}/api/admin/middleware/${pathStr}`
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getForwardHeaders(request),
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Middleware API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
const pathStr = path.join('/')
|
||||
const url = `${BACKEND_URL}/api/admin/middleware/${pathStr}`
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: getForwardHeaders(request),
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Middleware API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
const pathStr = path.join('/')
|
||||
const url = `${BACKEND_URL}/api/admin/middleware/${pathStr}`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: getForwardHeaders(request),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Middleware API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
59
admin-v2/app/api/admin/middleware/route.ts
Normal file
59
admin-v2/app/api/admin/middleware/route.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Middleware Admin API Proxy - Base route
|
||||
* GET /api/admin/middleware -> GET all middleware configs
|
||||
* Forwards authentication cookies for session-based auth
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
function getForwardHeaders(request: NextRequest): HeadersInit {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
// Forward cookie for session auth
|
||||
const cookie = request.headers.get('cookie')
|
||||
if (cookie) {
|
||||
headers['Cookie'] = cookie
|
||||
}
|
||||
|
||||
// Forward authorization header if present
|
||||
const auth = request.headers.get('authorization')
|
||||
if (auth) {
|
||||
headers['Authorization'] = auth
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams.toString()
|
||||
const url = `${BACKEND_URL}/api/admin/middleware${searchParams ? `?${searchParams}` : ''}`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getForwardHeaders(request),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Middleware API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
50
admin-v2/app/api/admin/night-mode/execute/route.ts
Normal file
50
admin-v2/app/api/admin/night-mode/execute/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Night Mode Execute API Route
|
||||
*
|
||||
* POST - Sofortige Ausführung (start/stop)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const NIGHT_SCHEDULER_URL = process.env.NIGHT_SCHEDULER_URL || 'http://night-scheduler:8096'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
if (!body.action || !['start', 'stop'].includes(body.action)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Aktion muss "start" oder "stop" sein' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(`${NIGHT_SCHEDULER_URL}/api/night-mode/execute`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Night-Scheduler Fehler: ${error}` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Night-Mode Execute API Error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Night-Scheduler nicht erreichbar',
|
||||
details: error instanceof Error ? error.message : 'Unbekannter Fehler',
|
||||
},
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
77
admin-v2/app/api/admin/night-mode/route.ts
Normal file
77
admin-v2/app/api/admin/night-mode/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Night Mode API Route
|
||||
*
|
||||
* Proxy für den night-scheduler Service (Port 8096)
|
||||
* GET - Status abrufen
|
||||
* POST - Konfiguration speichern
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const NIGHT_SCHEDULER_URL = process.env.NIGHT_SCHEDULER_URL || 'http://night-scheduler:8096'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const response = await fetch(`${NIGHT_SCHEDULER_URL}/api/night-mode`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Night-Scheduler Fehler: ${error}` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Night-Mode API Error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Night-Scheduler nicht erreichbar',
|
||||
details: error instanceof Error ? error.message : 'Unbekannter Fehler',
|
||||
},
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${NIGHT_SCHEDULER_URL}/api/night-mode`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Night-Scheduler Fehler: ${error}` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Night-Mode API Error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Night-Scheduler nicht erreichbar',
|
||||
details: error instanceof Error ? error.message : 'Unbekannter Fehler',
|
||||
},
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
41
admin-v2/app/api/admin/night-mode/services/route.ts
Normal file
41
admin-v2/app/api/admin/night-mode/services/route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Night Mode Services API Route
|
||||
*
|
||||
* GET - Liste aller Services abrufen
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
const NIGHT_SCHEDULER_URL = process.env.NIGHT_SCHEDULER_URL || 'http://night-scheduler:8096'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const response = await fetch(`${NIGHT_SCHEDULER_URL}/api/night-mode/services`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Night-Scheduler Fehler: ${error}` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Night-Mode Services API Error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Night-Scheduler nicht erreichbar',
|
||||
details: error instanceof Error ? error.message : 'Unbekannter Fehler',
|
||||
},
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
215
admin-v2/app/api/ai/rag-pipeline/route.ts
Normal file
215
admin-v2/app/api/ai/rag-pipeline/route.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
// Backend URL - klausur-service
|
||||
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://localhost:8086'
|
||||
|
||||
// Helper to proxy requests to backend
|
||||
async function proxyRequest(
|
||||
endpoint: string,
|
||||
method: string = 'GET',
|
||||
body?: any
|
||||
): Promise<Response> {
|
||||
const url = `${KLAUSUR_SERVICE_URL}${endpoint}`
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body)
|
||||
}
|
||||
|
||||
return fetch(url, options)
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const action = searchParams.get('action')
|
||||
const jobId = searchParams.get('job_id')
|
||||
const versionId = searchParams.get('version_id')
|
||||
|
||||
try {
|
||||
let response: Response
|
||||
|
||||
switch (action) {
|
||||
case 'jobs':
|
||||
response = await proxyRequest('/api/v1/admin/training/jobs')
|
||||
break
|
||||
|
||||
case 'job':
|
||||
if (!jobId) {
|
||||
return NextResponse.json({ error: 'job_id required' }, { status: 400 })
|
||||
}
|
||||
response = await proxyRequest(`/api/v1/admin/training/jobs/${jobId}`)
|
||||
break
|
||||
|
||||
case 'models':
|
||||
response = await proxyRequest('/api/v1/admin/training/models')
|
||||
break
|
||||
|
||||
case 'model':
|
||||
if (!versionId) {
|
||||
return NextResponse.json({ error: 'version_id required' }, { status: 400 })
|
||||
}
|
||||
response = await proxyRequest(`/api/v1/admin/training/models/${versionId}`)
|
||||
break
|
||||
|
||||
case 'dataset-stats':
|
||||
response = await proxyRequest('/api/v1/admin/training/dataset/stats')
|
||||
break
|
||||
|
||||
case 'status':
|
||||
response = await proxyRequest('/api/v1/admin/training/status')
|
||||
break
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Unknown action',
|
||||
validActions: ['jobs', 'job', 'models', 'model', 'dataset-stats', 'status']
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error(`Backend error: ${response.status} - ${errorText}`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Backend error', detail: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Training API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to backend', detail: String(error) },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const action = searchParams.get('action')
|
||||
const jobId = searchParams.get('job_id')
|
||||
const versionId = searchParams.get('version_id')
|
||||
|
||||
try {
|
||||
let response: Response
|
||||
let body: any = null
|
||||
|
||||
// Parse body if present
|
||||
try {
|
||||
const text = await request.text()
|
||||
if (text) {
|
||||
body = JSON.parse(text)
|
||||
}
|
||||
} catch {
|
||||
// No body or invalid JSON
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'create-job':
|
||||
if (!body) {
|
||||
return NextResponse.json({ error: 'Body required for job creation' }, { status: 400 })
|
||||
}
|
||||
response = await proxyRequest('/api/v1/admin/training/jobs', 'POST', body)
|
||||
break
|
||||
|
||||
case 'pause':
|
||||
if (!jobId) {
|
||||
return NextResponse.json({ error: 'job_id required' }, { status: 400 })
|
||||
}
|
||||
response = await proxyRequest(`/api/v1/admin/training/jobs/${jobId}/pause`, 'POST')
|
||||
break
|
||||
|
||||
case 'resume':
|
||||
if (!jobId) {
|
||||
return NextResponse.json({ error: 'job_id required' }, { status: 400 })
|
||||
}
|
||||
response = await proxyRequest(`/api/v1/admin/training/jobs/${jobId}/resume`, 'POST')
|
||||
break
|
||||
|
||||
case 'cancel':
|
||||
if (!jobId) {
|
||||
return NextResponse.json({ error: 'job_id required' }, { status: 400 })
|
||||
}
|
||||
response = await proxyRequest(`/api/v1/admin/training/jobs/${jobId}/cancel`, 'POST')
|
||||
break
|
||||
|
||||
case 'activate-model':
|
||||
if (!versionId) {
|
||||
return NextResponse.json({ error: 'version_id required' }, { status: 400 })
|
||||
}
|
||||
response = await proxyRequest(`/api/v1/admin/training/models/${versionId}/activate`, 'POST')
|
||||
break
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Unknown action',
|
||||
validActions: ['create-job', 'pause', 'resume', 'cancel', 'activate-model']
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }))
|
||||
return NextResponse.json(errorData, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Training API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to backend', detail: String(error) },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const jobId = searchParams.get('job_id')
|
||||
const versionId = searchParams.get('version_id')
|
||||
|
||||
try {
|
||||
let response: Response
|
||||
|
||||
if (jobId) {
|
||||
response = await proxyRequest(`/api/v1/admin/training/jobs/${jobId}`, 'DELETE')
|
||||
} else if (versionId) {
|
||||
response = await proxyRequest(`/api/v1/admin/training/models/${versionId}`, 'DELETE')
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: 'Either job_id or version_id required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }))
|
||||
return NextResponse.json(errorData, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Training API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to backend', detail: String(error) },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
172
admin-v2/app/api/alerts/[...path]/route.ts
Normal file
172
admin-v2/app/api/alerts/[...path]/route.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Alerts API Proxy - Catch-all route
|
||||
* Proxies all /api/alerts/* requests to backend
|
||||
* Supports: inbox, topics, rules, profile, stats, etc.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
function getForwardHeaders(request: NextRequest): HeadersInit {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
// Forward cookie for session auth
|
||||
const cookie = request.headers.get('cookie')
|
||||
if (cookie) {
|
||||
headers['Cookie'] = cookie
|
||||
}
|
||||
|
||||
// Forward authorization header if present
|
||||
const auth = request.headers.get('authorization')
|
||||
if (auth) {
|
||||
headers['Authorization'] = auth
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
const pathStr = path.join('/')
|
||||
const searchParams = request.nextUrl.searchParams.toString()
|
||||
const url = `${BACKEND_URL}/api/alerts/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getForwardHeaders(request),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Alerts API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
const pathStr = path.join('/')
|
||||
const url = `${BACKEND_URL}/api/alerts/${pathStr}`
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getForwardHeaders(request),
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Alerts API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
const pathStr = path.join('/')
|
||||
const url = `${BACKEND_URL}/api/alerts/${pathStr}`
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: getForwardHeaders(request),
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Alerts API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
const pathStr = path.join('/')
|
||||
const url = `${BACKEND_URL}/api/alerts/${pathStr}`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: getForwardHeaders(request),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Alerts API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
86
admin-v2/app/api/bqas/[...path]/route.ts
Normal file
86
admin-v2/app/api/bqas/[...path]/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* API Proxy for BQAS (Voice Service)
|
||||
* Forwards requests to the voice-service running on port 8091
|
||||
*/
|
||||
|
||||
// Internal Docker network uses HTTP, nginx handles HTTPS termination externally
|
||||
const VOICE_SERVICE_URL = process.env.VOICE_SERVICE_URL || 'http://voice-service:8091'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
const pathString = path.join('/')
|
||||
const searchParams = request.nextUrl.searchParams.toString()
|
||||
const url = `${VOICE_SERVICE_URL}/api/v1/bqas/${pathString}${searchParams ? `?${searchParams}` : ''}`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `Voice service returned ${response.status}` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('BQAS proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to voice service' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
const pathString = path.join('/')
|
||||
const url = `${VOICE_SERVICE_URL}/api/v1/bqas/${pathString}`
|
||||
|
||||
try {
|
||||
let body = null
|
||||
try {
|
||||
body = await request.json()
|
||||
} catch {
|
||||
// No body is fine for some POST requests
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `Voice service returned ${response.status}` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('BQAS proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to voice service' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
68
admin-v2/app/api/development/content/route.ts
Normal file
68
admin-v2/app/api/development/content/route.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Content API Route
|
||||
*
|
||||
* GET: Load current website content
|
||||
* POST: Save changed content (Admin only)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getContent, saveContent } from '@/lib/content'
|
||||
import type { WebsiteContent } from '@/lib/content-types'
|
||||
|
||||
// GET - Load content
|
||||
export async function GET() {
|
||||
try {
|
||||
const content = getContent()
|
||||
return NextResponse.json(content)
|
||||
} catch (error) {
|
||||
console.error('Error loading content:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to load content' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Save content
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Simple admin check via header or query
|
||||
// In production: JWT/Session-based auth
|
||||
const adminKey = request.headers.get('x-admin-key')
|
||||
const expectedKey = process.env.ADMIN_API_KEY || 'breakpilot-admin-2024'
|
||||
|
||||
if (adminKey !== expectedKey) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const content: WebsiteContent = await request.json()
|
||||
|
||||
// Validation
|
||||
if (!content.hero || !content.features || !content.faq || !content.pricing) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid content structure' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const result = saveContent(content)
|
||||
|
||||
if (result.success) {
|
||||
return NextResponse.json({ success: true, message: 'Content saved' })
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: result.error || 'Failed to save content' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving content:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to save content' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
100
admin-v2/app/api/dsfa-corpus/route.ts
Normal file
100
admin-v2/app/api/dsfa-corpus/route.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* DSFA Corpus API Proxy
|
||||
*
|
||||
* Proxies requests to klausur-service for DSFA RAG operations.
|
||||
* Endpoints: /api/v1/dsfa-rag/stats, /api/v1/dsfa-rag/sources
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const action = searchParams.get('action')
|
||||
|
||||
try {
|
||||
let url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag`
|
||||
|
||||
switch (action) {
|
||||
case 'status':
|
||||
url += '/stats'
|
||||
break
|
||||
case 'sources':
|
||||
url += '/sources'
|
||||
break
|
||||
case 'source-detail': {
|
||||
const code = searchParams.get('code')
|
||||
if (!code) {
|
||||
return NextResponse.json({ error: 'Missing code parameter' }, { status: 400 })
|
||||
}
|
||||
url += `/sources/${encodeURIComponent(code)}`
|
||||
break
|
||||
}
|
||||
default:
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
return NextResponse.json(data, { status: res.status })
|
||||
} catch (error) {
|
||||
console.error('DSFA corpus proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to klausur-service' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const action = searchParams.get('action')
|
||||
|
||||
try {
|
||||
let url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag`
|
||||
|
||||
switch (action) {
|
||||
case 'init': {
|
||||
url += '/init'
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
const data = await res.json()
|
||||
return NextResponse.json(data, { status: res.status })
|
||||
}
|
||||
|
||||
case 'ingest': {
|
||||
const body = await request.json()
|
||||
const sourceCode = body.source_code
|
||||
if (!sourceCode) {
|
||||
return NextResponse.json({ error: 'Missing source_code' }, { status: 400 })
|
||||
}
|
||||
url += `/sources/${encodeURIComponent(sourceCode)}/ingest`
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
const data = await res.json()
|
||||
return NextResponse.json(data, { status: res.status })
|
||||
}
|
||||
|
||||
default:
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('DSFA corpus proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to klausur-service' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
246
admin-v2/app/api/education/abitur-archiv/route.ts
Normal file
246
admin-v2/app/api/education/abitur-archiv/route.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* Abitur-Archiv API Route
|
||||
* Extends abitur-docs with theme search and enhanced filtering
|
||||
*/
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
|
||||
// Check for theme/semantic search
|
||||
const thema = searchParams.get('thema')
|
||||
|
||||
if (thema) {
|
||||
// Use semantic search endpoint
|
||||
return await handleSemanticSearch(thema, searchParams)
|
||||
}
|
||||
|
||||
// Forward all query params to backend abitur-docs
|
||||
const queryString = searchParams.toString()
|
||||
const url = `${BACKEND_URL}/api/abitur-docs/${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// Return mock data for development if backend is not available
|
||||
if (response.status === 404 || response.status === 502) {
|
||||
return NextResponse.json(getMockDocuments(searchParams))
|
||||
}
|
||||
throw new Error(`Backend responded with ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// If backend returns empty, use mock data for demo
|
||||
if (data.documents && Array.isArray(data.documents) && data.documents.length === 0 && data.total === 0) {
|
||||
return NextResponse.json(getMockDocuments(searchParams))
|
||||
}
|
||||
|
||||
// Enhance response with theme information
|
||||
return NextResponse.json({
|
||||
...data,
|
||||
themes: extractThemes(data.documents || [])
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Abitur-Archiv error:', error)
|
||||
return NextResponse.json(getMockDocuments(new URL(request.url).searchParams))
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSemanticSearch(thema: string, searchParams: URLSearchParams) {
|
||||
try {
|
||||
// Try to call RAG search endpoint
|
||||
const url = `${BACKEND_URL}/api/rag/search`
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: thema,
|
||||
collection: 'abitur_documents',
|
||||
limit: parseInt(searchParams.get('limit') || '20'),
|
||||
filters: {
|
||||
fach: searchParams.get('fach') || undefined,
|
||||
jahr: searchParams.get('jahr') ? parseInt(searchParams.get('jahr')!) : undefined,
|
||||
bundesland: searchParams.get('bundesland') || undefined,
|
||||
niveau: searchParams.get('niveau') || undefined,
|
||||
typ: searchParams.get('typ') || undefined,
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
return NextResponse.json({
|
||||
documents: data.results || [],
|
||||
total: data.total || 0,
|
||||
page: 1,
|
||||
limit: parseInt(searchParams.get('limit') || '20'),
|
||||
total_pages: 1,
|
||||
search_query: thema
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('RAG search not available, falling back to mock')
|
||||
}
|
||||
|
||||
// Fallback to filtered mock data
|
||||
return NextResponse.json(getMockDocumentsWithTheme(thema, searchParams))
|
||||
}
|
||||
|
||||
function getMockDocuments(searchParams: URLSearchParams) {
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '20')
|
||||
const fach = searchParams.get('fach')
|
||||
const jahr = searchParams.get('jahr')
|
||||
const bundesland = searchParams.get('bundesland')
|
||||
const niveau = searchParams.get('niveau')
|
||||
const typ = searchParams.get('typ')
|
||||
|
||||
// Generate mock documents
|
||||
const allDocs = generateMockDocs()
|
||||
|
||||
// Apply filters
|
||||
let filtered = allDocs
|
||||
if (fach) filtered = filtered.filter(d => d.fach === fach)
|
||||
if (jahr) filtered = filtered.filter(d => d.jahr === parseInt(jahr))
|
||||
if (bundesland) filtered = filtered.filter(d => d.bundesland === bundesland)
|
||||
if (niveau) filtered = filtered.filter(d => d.niveau === niveau)
|
||||
if (typ) filtered = filtered.filter(d => d.typ === typ)
|
||||
|
||||
// Paginate
|
||||
const start = (page - 1) * limit
|
||||
const docs = filtered.slice(start, start + limit)
|
||||
|
||||
return {
|
||||
documents: docs,
|
||||
total: filtered.length,
|
||||
page,
|
||||
limit,
|
||||
total_pages: Math.ceil(filtered.length / limit),
|
||||
themes: extractThemes(docs)
|
||||
}
|
||||
}
|
||||
|
||||
function getMockDocumentsWithTheme(thema: string, searchParams: URLSearchParams) {
|
||||
const limit = parseInt(searchParams.get('limit') || '20')
|
||||
const allDocs = generateMockDocs()
|
||||
|
||||
// Simple theme matching (in production this would be semantic search)
|
||||
const themaLower = thema.toLowerCase()
|
||||
let filtered = allDocs
|
||||
|
||||
// Match theme to aufgabentyp keywords
|
||||
if (themaLower.includes('gedicht')) {
|
||||
filtered = filtered.filter(d => d.themes?.includes('gedichtanalyse'))
|
||||
} else if (themaLower.includes('drama')) {
|
||||
filtered = filtered.filter(d => d.themes?.includes('dramenanalyse'))
|
||||
} else if (themaLower.includes('prosa') || themaLower.includes('roman')) {
|
||||
filtered = filtered.filter(d => d.themes?.includes('prosaanalyse'))
|
||||
} else if (themaLower.includes('eroerterung')) {
|
||||
filtered = filtered.filter(d => d.themes?.includes('eroerterung'))
|
||||
} else if (themaLower.includes('text') || themaLower.includes('analyse')) {
|
||||
filtered = filtered.filter(d => d.themes?.includes('textanalyse'))
|
||||
}
|
||||
|
||||
// Apply additional filters
|
||||
const fach = searchParams.get('fach')
|
||||
const jahr = searchParams.get('jahr')
|
||||
if (fach) filtered = filtered.filter(d => d.fach === fach)
|
||||
if (jahr) filtered = filtered.filter(d => d.jahr === parseInt(jahr))
|
||||
|
||||
return {
|
||||
documents: filtered.slice(0, limit),
|
||||
total: filtered.length,
|
||||
page: 1,
|
||||
limit,
|
||||
total_pages: Math.ceil(filtered.length / limit),
|
||||
search_query: thema,
|
||||
themes: extractThemes(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
function generateMockDocs() {
|
||||
const faecher = ['deutsch', 'englisch']
|
||||
const jahre = [2021, 2022, 2023, 2024, 2025]
|
||||
const niveaus: Array<'eA' | 'gA'> = ['eA', 'gA']
|
||||
const typen: Array<'aufgabe' | 'erwartungshorizont'> = ['aufgabe', 'erwartungshorizont']
|
||||
const aufgabentypen = [
|
||||
{ nummer: 'I', themes: ['textanalyse', 'sachtext'] },
|
||||
{ nummer: 'II', themes: ['gedichtanalyse', 'lyrik'] },
|
||||
{ nummer: 'III', themes: ['prosaanalyse', 'epik'] },
|
||||
]
|
||||
|
||||
const docs = []
|
||||
let id = 1
|
||||
|
||||
for (const jahr of jahre) {
|
||||
for (const fach of faecher) {
|
||||
for (const niveau of niveaus) {
|
||||
for (const aufgabe of aufgabentypen) {
|
||||
for (const typ of typen) {
|
||||
const suffix = typ === 'erwartungshorizont' ? '_EWH' : ''
|
||||
const dateiname = `${jahr}_${capitalize(fach)}_${niveau}_Aufgabe_${aufgabe.nummer}${suffix}.pdf`
|
||||
|
||||
docs.push({
|
||||
id: `doc-${id++}`,
|
||||
dateiname,
|
||||
original_dateiname: dateiname,
|
||||
bundesland: 'niedersachsen',
|
||||
fach,
|
||||
jahr,
|
||||
niveau,
|
||||
typ,
|
||||
aufgaben_nummer: aufgabe.nummer,
|
||||
themes: aufgabe.themes,
|
||||
status: 'indexed' as const,
|
||||
confidence: 0.92 + Math.random() * 0.08,
|
||||
file_path: `/api/education/abitur-archiv/file/${dateiname}`,
|
||||
file_size: Math.floor(Math.random() * 500000) + 100000,
|
||||
indexed: true,
|
||||
vector_ids: [`vec-${id}-1`, `vec-${id}-2`],
|
||||
created_at: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return docs
|
||||
}
|
||||
|
||||
function extractThemes(documents: any[]) {
|
||||
const themeCounts = new Map<string, number>()
|
||||
|
||||
for (const doc of documents) {
|
||||
const themes = doc.themes || []
|
||||
for (const theme of themes) {
|
||||
themeCounts.set(theme, (themeCounts.get(theme) || 0) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(themeCounts.entries())
|
||||
.map(([label, count]) => ({
|
||||
label: capitalize(label),
|
||||
count,
|
||||
aufgabentyp: label,
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 10)
|
||||
}
|
||||
|
||||
function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
105
admin-v2/app/api/education/abitur-archiv/suggest/route.ts
Normal file
105
admin-v2/app/api/education/abitur-archiv/suggest/route.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* Theme Suggestions API for Abitur-Archiv
|
||||
* Returns autocomplete suggestions for semantic search
|
||||
*/
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const query = searchParams.get('q') || ''
|
||||
|
||||
if (query.length < 2) {
|
||||
return NextResponse.json({ suggestions: [], query })
|
||||
}
|
||||
|
||||
// Try to get suggestions from backend
|
||||
try {
|
||||
const url = `${BACKEND_URL}/api/abitur-archiv/suggest?q=${encodeURIComponent(query)}`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Backend suggest not available, using static suggestions')
|
||||
}
|
||||
|
||||
// Fallback to static suggestions
|
||||
return NextResponse.json({
|
||||
suggestions: getStaticSuggestions(query),
|
||||
query
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Suggest error:', error)
|
||||
return NextResponse.json({ suggestions: [], query: '' })
|
||||
}
|
||||
}
|
||||
|
||||
function getStaticSuggestions(query: string) {
|
||||
const allSuggestions = [
|
||||
// Textanalyse
|
||||
{ label: 'Textanalyse', count: 45, aufgabentyp: 'textanalyse', kategorie: 'Analyse' },
|
||||
{ label: 'Textanalyse Sachtext', count: 28, aufgabentyp: 'textanalyse_pragmatisch', kategorie: 'Analyse' },
|
||||
{ label: 'Textanalyse Rede', count: 12, aufgabentyp: 'textanalyse_rede', kategorie: 'Analyse' },
|
||||
{ label: 'Textanalyse Kommentar', count: 8, aufgabentyp: 'textanalyse_kommentar', kategorie: 'Analyse' },
|
||||
|
||||
// Gedichtanalyse
|
||||
{ label: 'Gedichtanalyse', count: 38, aufgabentyp: 'gedichtanalyse', kategorie: 'Lyrik' },
|
||||
{ label: 'Gedichtanalyse Romantik', count: 15, aufgabentyp: 'gedichtanalyse', zeitraum: 'Romantik', kategorie: 'Lyrik' },
|
||||
{ label: 'Gedichtanalyse Expressionismus', count: 12, aufgabentyp: 'gedichtanalyse', zeitraum: 'Expressionismus', kategorie: 'Lyrik' },
|
||||
{ label: 'Gedichtanalyse Barock', count: 8, aufgabentyp: 'gedichtanalyse', zeitraum: 'Barock', kategorie: 'Lyrik' },
|
||||
{ label: 'Gedichtanalyse Klassik', count: 10, aufgabentyp: 'gedichtanalyse', zeitraum: 'Klassik', kategorie: 'Lyrik' },
|
||||
{ label: 'Gedichtanalyse Moderne', count: 14, aufgabentyp: 'gedichtanalyse', zeitraum: 'Moderne', kategorie: 'Lyrik' },
|
||||
{ label: 'Gedichtvergleich', count: 18, aufgabentyp: 'gedichtvergleich', kategorie: 'Lyrik' },
|
||||
|
||||
// Dramenanalyse
|
||||
{ label: 'Dramenanalyse', count: 28, aufgabentyp: 'dramenanalyse', kategorie: 'Drama' },
|
||||
{ label: 'Dramenanalyse Faust', count: 14, aufgabentyp: 'dramenanalyse', kategorie: 'Drama' },
|
||||
{ label: 'Dramenanalyse Woyzeck', count: 8, aufgabentyp: 'dramenanalyse', kategorie: 'Drama' },
|
||||
{ label: 'Episches Theater Brecht', count: 10, aufgabentyp: 'dramenanalyse', kategorie: 'Drama' },
|
||||
{ label: 'Szenenanalyse', count: 22, aufgabentyp: 'szenenanalyse', kategorie: 'Drama' },
|
||||
|
||||
// Prosaanalyse
|
||||
{ label: 'Prosaanalyse', count: 25, aufgabentyp: 'prosaanalyse', kategorie: 'Epik' },
|
||||
{ label: 'Romananalyse', count: 18, aufgabentyp: 'prosaanalyse', kategorie: 'Epik' },
|
||||
{ label: 'Kurzgeschichte', count: 20, aufgabentyp: 'prosaanalyse', kategorie: 'Epik' },
|
||||
{ label: 'Novelle', count: 12, aufgabentyp: 'prosaanalyse', kategorie: 'Epik' },
|
||||
{ label: 'Erzaehlung', count: 15, aufgabentyp: 'prosaanalyse', kategorie: 'Epik' },
|
||||
|
||||
// Eroerterung
|
||||
{ label: 'Eroerterung', count: 32, aufgabentyp: 'eroerterung', kategorie: 'Argumentation' },
|
||||
{ label: 'Eroerterung textgebunden', count: 18, aufgabentyp: 'eroerterung_textgebunden', kategorie: 'Argumentation' },
|
||||
{ label: 'Eroerterung materialgestuetzt', count: 14, aufgabentyp: 'eroerterung_materialgestuetzt', kategorie: 'Argumentation' },
|
||||
{ label: 'Stellungnahme', count: 10, aufgabentyp: 'stellungnahme', kategorie: 'Argumentation' },
|
||||
|
||||
// Sprachreflexion
|
||||
{ label: 'Sprachreflexion', count: 15, aufgabentyp: 'sprachreflexion', kategorie: 'Sprache' },
|
||||
{ label: 'Sprachwandel', count: 8, aufgabentyp: 'sprachreflexion', kategorie: 'Sprache' },
|
||||
{ label: 'Sprachkritik', count: 6, aufgabentyp: 'sprachreflexion', kategorie: 'Sprache' },
|
||||
{ label: 'Kommunikation', count: 10, aufgabentyp: 'kommunikation', kategorie: 'Sprache' },
|
||||
|
||||
// Vergleich
|
||||
{ label: 'Vergleichende Analyse', count: 20, aufgabentyp: 'vergleich', kategorie: 'Vergleich' },
|
||||
{ label: 'Epochenvergleich', count: 12, aufgabentyp: 'epochenvergleich', kategorie: 'Vergleich' },
|
||||
]
|
||||
|
||||
const queryLower = query.toLowerCase()
|
||||
|
||||
// Filter suggestions based on query
|
||||
return allSuggestions
|
||||
.filter(s =>
|
||||
s.label.toLowerCase().includes(queryLower) ||
|
||||
s.aufgabentyp.toLowerCase().includes(queryLower) ||
|
||||
(s.zeitraum && s.zeitraum.toLowerCase().includes(queryLower)) ||
|
||||
s.kategorie.toLowerCase().includes(queryLower)
|
||||
)
|
||||
.slice(0, 8)
|
||||
}
|
||||
139
admin-v2/app/api/education/abitur-docs/route.ts
Normal file
139
admin-v2/app/api/education/abitur-docs/route.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* Proxy to backend /api/abitur-docs
|
||||
* Lists and manages Abitur documents (NiBiS, etc.)
|
||||
*/
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
|
||||
// Forward all query params to backend
|
||||
const queryString = searchParams.toString()
|
||||
const url = `${BACKEND_URL}/api/abitur-docs/${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// Return mock data for development if backend is not available
|
||||
if (response.status === 404 || response.status === 502) {
|
||||
return NextResponse.json(getMockDocuments(searchParams))
|
||||
}
|
||||
throw new Error(`Backend responded with ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// If backend returns empty array, use mock data for demo purposes
|
||||
// (Backend uses in-memory storage which is lost on restart)
|
||||
if (Array.isArray(data) && data.length === 0) {
|
||||
console.log('Backend returned empty array, using mock data')
|
||||
return NextResponse.json(getMockDocuments(searchParams))
|
||||
}
|
||||
|
||||
// Handle paginated response with empty documents
|
||||
if (data.documents && Array.isArray(data.documents) && data.documents.length === 0 && data.total === 0) {
|
||||
console.log('Backend returned empty documents, using mock data')
|
||||
return NextResponse.json(getMockDocuments(searchParams))
|
||||
}
|
||||
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Abitur docs list error:', error)
|
||||
// Return mock data for development
|
||||
return NextResponse.json(getMockDocuments(new URL(request.url).searchParams))
|
||||
}
|
||||
}
|
||||
|
||||
function getMockDocuments(searchParams: URLSearchParams) {
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '20')
|
||||
const fach = searchParams.get('fach')
|
||||
const jahr = searchParams.get('jahr')
|
||||
const bundesland = searchParams.get('bundesland')
|
||||
|
||||
// Generate mock documents
|
||||
const allDocs = generateMockDocs()
|
||||
|
||||
// Apply filters
|
||||
let filtered = allDocs
|
||||
if (fach) {
|
||||
filtered = filtered.filter(d => d.fach === fach)
|
||||
}
|
||||
if (jahr) {
|
||||
filtered = filtered.filter(d => d.jahr === parseInt(jahr))
|
||||
}
|
||||
if (bundesland) {
|
||||
filtered = filtered.filter(d => d.bundesland === bundesland)
|
||||
}
|
||||
|
||||
// Paginate
|
||||
const start = (page - 1) * limit
|
||||
const docs = filtered.slice(start, start + limit)
|
||||
|
||||
return {
|
||||
documents: docs,
|
||||
total: filtered.length,
|
||||
page,
|
||||
limit,
|
||||
total_pages: Math.ceil(filtered.length / limit),
|
||||
}
|
||||
}
|
||||
|
||||
function generateMockDocs() {
|
||||
const faecher = ['deutsch', 'mathematik', 'englisch', 'biologie', 'physik', 'chemie', 'geschichte']
|
||||
const jahre = [2024, 2025]
|
||||
const niveaus = ['eA', 'gA']
|
||||
const typen = ['aufgabe', 'erwartungshorizont']
|
||||
const nummern = ['I', 'II', 'III']
|
||||
|
||||
const docs = []
|
||||
let id = 1
|
||||
|
||||
for (const jahr of jahre) {
|
||||
for (const fach of faecher) {
|
||||
for (const niveau of niveaus) {
|
||||
for (const nummer of nummern) {
|
||||
for (const typ of typen) {
|
||||
const suffix = typ === 'erwartungshorizont' ? '_EWH' : ''
|
||||
const dateiname = `${jahr}_${capitalize(fach)}_${niveau}_${nummer}${suffix}.pdf`
|
||||
|
||||
docs.push({
|
||||
id: `doc-${id++}`,
|
||||
dateiname,
|
||||
original_dateiname: dateiname,
|
||||
bundesland: 'niedersachsen',
|
||||
fach,
|
||||
jahr,
|
||||
niveau,
|
||||
typ,
|
||||
aufgaben_nummer: nummer,
|
||||
status: 'indexed',
|
||||
confidence: 0.95,
|
||||
file_path: `/tmp/abitur-docs/${dateiname}`,
|
||||
file_size: Math.floor(Math.random() * 500000) + 100000,
|
||||
indexed: true,
|
||||
vector_ids: [`vec-${id}-1`, `vec-${id}-2`],
|
||||
created_at: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return docs
|
||||
}
|
||||
|
||||
function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
395
admin-v2/app/api/infrastructure/log-extract/extract/route.ts
Normal file
395
admin-v2/app/api/infrastructure/log-extract/extract/route.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import type { ExtractedError, ErrorCategory, LogExtractionResponse } from '@/types/infrastructure-modules'
|
||||
|
||||
// Woodpecker API configuration
|
||||
const WOODPECKER_URL = process.env.WOODPECKER_URL || 'http://woodpecker-server:8000'
|
||||
const WOODPECKER_TOKEN = process.env.WOODPECKER_TOKEN || ''
|
||||
|
||||
// =============================================================================
|
||||
// Error Pattern Matching
|
||||
// =============================================================================
|
||||
|
||||
interface ErrorPattern {
|
||||
pattern: RegExp
|
||||
category: ErrorCategory
|
||||
extractMessage?: (match: RegExpMatchArray, line: string) => string
|
||||
}
|
||||
|
||||
/**
|
||||
* Patterns fuer verschiedene Fehlertypen in CI/CD Logs
|
||||
*/
|
||||
const ERROR_PATTERNS: ErrorPattern[] = [
|
||||
// Test Failures
|
||||
{
|
||||
pattern: /^(FAIL|FAILED|ERROR):?\s+(.+)$/i,
|
||||
category: 'test_failure',
|
||||
extractMessage: (match, line) => match[2] || line,
|
||||
},
|
||||
{
|
||||
pattern: /^---\s+FAIL:\s+(.+)\s+\([\d.]+s\)$/,
|
||||
category: 'test_failure',
|
||||
extractMessage: (match) => `Test failed: ${match[1]}`,
|
||||
},
|
||||
{
|
||||
pattern: /pytest.*FAILED\s+(.+)$/,
|
||||
category: 'test_failure',
|
||||
extractMessage: (match) => `pytest: ${match[1]}`,
|
||||
},
|
||||
{
|
||||
pattern: /AssertionError:\s+(.+)$/,
|
||||
category: 'test_failure',
|
||||
extractMessage: (match) => `Assertion failed: ${match[1]}`,
|
||||
},
|
||||
{
|
||||
pattern: /FAIL\s+[\w\/]+\s+\[build failed\]/,
|
||||
category: 'build_error',
|
||||
},
|
||||
|
||||
// Build Errors
|
||||
{
|
||||
pattern: /^(error|Error)\[[\w-]+\]:\s+(.+)$/,
|
||||
category: 'build_error',
|
||||
extractMessage: (match) => match[2],
|
||||
},
|
||||
{
|
||||
pattern: /cannot find (module|package)\s+["'](.+)["']/i,
|
||||
category: 'build_error',
|
||||
extractMessage: (match) => `Missing ${match[1]}: ${match[2]}`,
|
||||
},
|
||||
{
|
||||
pattern: /undefined:\s+(.+)$/,
|
||||
category: 'build_error',
|
||||
extractMessage: (match) => `Undefined: ${match[1]}`,
|
||||
},
|
||||
{
|
||||
pattern: /compilation failed/i,
|
||||
category: 'build_error',
|
||||
},
|
||||
{
|
||||
pattern: /npm ERR!\s+(.+)$/,
|
||||
category: 'build_error',
|
||||
extractMessage: (match) => `npm error: ${match[1]}`,
|
||||
},
|
||||
{
|
||||
pattern: /go:\s+(.+):\s+(.+)$/,
|
||||
category: 'build_error',
|
||||
extractMessage: (match) => `Go: ${match[1]}: ${match[2]}`,
|
||||
},
|
||||
|
||||
// Security Warnings
|
||||
{
|
||||
pattern: /\[CRITICAL\]\s+(.+)$/i,
|
||||
category: 'security_warning',
|
||||
extractMessage: (match) => `Critical: ${match[1]}`,
|
||||
},
|
||||
{
|
||||
pattern: /\[HIGH\]\s+(.+)$/i,
|
||||
category: 'security_warning',
|
||||
extractMessage: (match) => `High severity: ${match[1]}`,
|
||||
},
|
||||
{
|
||||
pattern: /CVE-\d{4}-\d+/,
|
||||
category: 'security_warning',
|
||||
extractMessage: (match, line) => line.trim(),
|
||||
},
|
||||
{
|
||||
pattern: /vulnerability found/i,
|
||||
category: 'security_warning',
|
||||
},
|
||||
{
|
||||
pattern: /secret.*detected/i,
|
||||
category: 'security_warning',
|
||||
},
|
||||
{
|
||||
pattern: /gitleaks.*found/i,
|
||||
category: 'security_warning',
|
||||
},
|
||||
{
|
||||
pattern: /semgrep.*finding/i,
|
||||
category: 'security_warning',
|
||||
},
|
||||
|
||||
// License Violations
|
||||
{
|
||||
pattern: /license.*violation/i,
|
||||
category: 'license_violation',
|
||||
},
|
||||
{
|
||||
pattern: /incompatible license/i,
|
||||
category: 'license_violation',
|
||||
},
|
||||
{
|
||||
pattern: /AGPL|GPL-3|SSPL/,
|
||||
category: 'license_violation',
|
||||
extractMessage: (match, line) => `Potentially problematic license found: ${match[0]}`,
|
||||
},
|
||||
|
||||
// Dependency Issues
|
||||
{
|
||||
pattern: /dependency.*not found/i,
|
||||
category: 'dependency_issue',
|
||||
},
|
||||
{
|
||||
pattern: /outdated.*dependency/i,
|
||||
category: 'dependency_issue',
|
||||
},
|
||||
{
|
||||
pattern: /version conflict/i,
|
||||
category: 'dependency_issue',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Patterns to extract file paths from error lines
|
||||
*/
|
||||
const FILE_PATH_PATTERNS = [
|
||||
/([\/\w.-]+\.(go|py|ts|tsx|js|jsx|rs)):(\d+)/,
|
||||
/File "([^"]+)", line (\d+)/,
|
||||
/at ([\/\w.-]+):(\d+):\d+/,
|
||||
]
|
||||
|
||||
/**
|
||||
* Patterns to extract service names from log lines or paths
|
||||
*/
|
||||
const SERVICE_PATTERNS = [
|
||||
/service[s]?\/([a-z-]+)/i,
|
||||
/\/([a-z-]+-service)\//i,
|
||||
/^([a-z-]+):\s/,
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// Log Parsing Functions
|
||||
// =============================================================================
|
||||
|
||||
interface LogLine {
|
||||
pos: number
|
||||
out: string
|
||||
time: number
|
||||
}
|
||||
|
||||
function extractFilePath(line: string): { path?: string; lineNumber?: number } {
|
||||
for (const pattern of FILE_PATH_PATTERNS) {
|
||||
const match = line.match(pattern)
|
||||
if (match) {
|
||||
return {
|
||||
path: match[1],
|
||||
lineNumber: parseInt(match[2] || match[3], 10) || undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function extractService(line: string, filePath?: string): string | undefined {
|
||||
// First try to extract from file path
|
||||
if (filePath) {
|
||||
for (const pattern of SERVICE_PATTERNS) {
|
||||
const match = filePath.match(pattern)
|
||||
if (match) return match[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Then try from the line itself
|
||||
for (const pattern of SERVICE_PATTERNS) {
|
||||
const match = line.match(pattern)
|
||||
if (match) return match[1]
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function parseLogLines(logs: LogLine[], stepName: string): ExtractedError[] {
|
||||
const errors: ExtractedError[] = []
|
||||
const seenMessages = new Set<string>()
|
||||
|
||||
for (const logLine of logs) {
|
||||
const line = logLine.out.trim()
|
||||
if (!line) continue
|
||||
|
||||
for (const errorPattern of ERROR_PATTERNS) {
|
||||
const match = line.match(errorPattern.pattern)
|
||||
if (match) {
|
||||
const message = errorPattern.extractMessage
|
||||
? errorPattern.extractMessage(match, line)
|
||||
: line
|
||||
|
||||
// Deduplicate similar errors
|
||||
const messageKey = `${errorPattern.category}:${message.substring(0, 100)}`
|
||||
if (seenMessages.has(messageKey)) continue
|
||||
seenMessages.add(messageKey)
|
||||
|
||||
const fileInfo = extractFilePath(line)
|
||||
const service = extractService(line, fileInfo.path)
|
||||
|
||||
errors.push({
|
||||
step: stepName,
|
||||
line: logLine.pos,
|
||||
message,
|
||||
category: errorPattern.category,
|
||||
file_path: fileInfo.path,
|
||||
service,
|
||||
})
|
||||
|
||||
break // Only match first pattern per line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API Handler
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* POST /api/infrastructure/logs/extract
|
||||
*
|
||||
* Extrahiert Fehler aus Woodpecker Pipeline Logs.
|
||||
*
|
||||
* Request Body:
|
||||
* - pipeline_number: number (required)
|
||||
* - repo_id?: string (default: '1')
|
||||
*
|
||||
* Response:
|
||||
* - errors: ExtractedError[]
|
||||
* - pipeline_number: number
|
||||
* - extracted_at: string
|
||||
* - lines_parsed: number
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { pipeline_number, repo_id = '1' } = body
|
||||
|
||||
if (!pipeline_number) {
|
||||
return NextResponse.json(
|
||||
{ error: 'pipeline_number ist erforderlich' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// 1. Fetch pipeline details to get step IDs
|
||||
const pipelineResponse = await fetch(
|
||||
`${WOODPECKER_URL}/api/repos/${repo_id}/pipelines/${pipeline_number}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
}
|
||||
)
|
||||
|
||||
if (!pipelineResponse.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `Pipeline ${pipeline_number} nicht gefunden` },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const pipeline = await pipelineResponse.json()
|
||||
|
||||
// 2. Extract step IDs from workflows
|
||||
const failedSteps: { id: number; name: string }[] = []
|
||||
|
||||
if (pipeline.workflows) {
|
||||
for (const workflow of pipeline.workflows) {
|
||||
if (workflow.children) {
|
||||
for (const child of workflow.children) {
|
||||
if (child.state === 'failure' || child.state === 'error') {
|
||||
failedSteps.push({
|
||||
id: child.id,
|
||||
name: child.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fetch logs for each failed step
|
||||
const allErrors: ExtractedError[] = []
|
||||
let totalLinesParsed = 0
|
||||
|
||||
for (const step of failedSteps) {
|
||||
try {
|
||||
const logsResponse = await fetch(
|
||||
`${WOODPECKER_URL}/api/repos/${repo_id}/pipelines/${pipeline_number}/logs/${step.id}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (logsResponse.ok) {
|
||||
const logs: LogLine[] = await logsResponse.json()
|
||||
totalLinesParsed += logs.length
|
||||
|
||||
const stepErrors = parseLogLines(logs, step.name)
|
||||
allErrors.push(...stepErrors)
|
||||
}
|
||||
} catch (logError) {
|
||||
console.error(`Failed to fetch logs for step ${step.name}:`, logError)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Sort errors by severity (security > license > build > test > dependency)
|
||||
const categoryPriority: Record<ErrorCategory, number> = {
|
||||
'security_warning': 1,
|
||||
'license_violation': 2,
|
||||
'build_error': 3,
|
||||
'test_failure': 4,
|
||||
'dependency_issue': 5,
|
||||
}
|
||||
|
||||
allErrors.sort((a, b) => categoryPriority[a.category] - categoryPriority[b.category])
|
||||
|
||||
const response: LogExtractionResponse = {
|
||||
errors: allErrors,
|
||||
pipeline_number,
|
||||
extracted_at: new Date().toISOString(),
|
||||
lines_parsed: totalLinesParsed,
|
||||
}
|
||||
|
||||
return NextResponse.json(response)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Log extraction error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Fehler bei der Log-Extraktion' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/infrastructure/logs/extract?pipeline_number=123
|
||||
*
|
||||
* Convenience method - calls POST internally
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const pipeline_number = searchParams.get('pipeline_number')
|
||||
const repo_id = searchParams.get('repo_id') || '1'
|
||||
|
||||
if (!pipeline_number) {
|
||||
return NextResponse.json(
|
||||
{ error: 'pipeline_number Query-Parameter ist erforderlich' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create a mock request with JSON body
|
||||
const mockRequest = new NextRequest(request.url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ pipeline_number: parseInt(pipeline_number, 10), repo_id }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
return POST(mockRequest)
|
||||
}
|
||||
180
admin-v2/app/api/legal-corpus/route.ts
Normal file
180
admin-v2/app/api/legal-corpus/route.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Legal Corpus API Proxy
|
||||
*
|
||||
* Proxies requests to klausur-service for RAG operations.
|
||||
* This allows the client-side RAG page to call the API without CORS issues.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
|
||||
const QDRANT_URL = process.env.QDRANT_URL || 'http://qdrant:6333'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const action = searchParams.get('action')
|
||||
|
||||
try {
|
||||
let url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus`
|
||||
|
||||
switch (action) {
|
||||
case 'status': {
|
||||
// Query Qdrant directly for collection stats
|
||||
const qdrantRes = await fetch(`${QDRANT_URL}/collections/bp_legal_corpus`, {
|
||||
cache: 'no-store',
|
||||
})
|
||||
if (!qdrantRes.ok) {
|
||||
return NextResponse.json({ error: 'Qdrant not available' }, { status: 503 })
|
||||
}
|
||||
const qdrantData = await qdrantRes.json()
|
||||
const result = qdrantData.result || {}
|
||||
return NextResponse.json({
|
||||
collection: 'bp_legal_corpus',
|
||||
totalPoints: result.points_count || 0,
|
||||
vectorSize: result.config?.params?.vectors?.size || 0,
|
||||
status: result.status || 'unknown',
|
||||
regulations: {},
|
||||
})
|
||||
}
|
||||
case 'search':
|
||||
const query = searchParams.get('query')
|
||||
const topK = searchParams.get('top_k') || '5'
|
||||
const regulations = searchParams.get('regulations')
|
||||
url += `/search?query=${encodeURIComponent(query || '')}&top_k=${topK}`
|
||||
if (regulations) {
|
||||
url += `®ulations=${encodeURIComponent(regulations)}`
|
||||
}
|
||||
break
|
||||
case 'ingestion-status':
|
||||
url += '/ingestion-status'
|
||||
break
|
||||
case 'regulations':
|
||||
url += '/regulations'
|
||||
break
|
||||
case 'custom-documents':
|
||||
url += '/custom-documents'
|
||||
break
|
||||
case 'pipeline-checkpoints':
|
||||
url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/pipeline/checkpoints`
|
||||
break
|
||||
case 'pipeline-status':
|
||||
url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/pipeline/status`
|
||||
break
|
||||
case 'traceability': {
|
||||
const chunkId = searchParams.get('chunk_id')
|
||||
const regulation = searchParams.get('regulation')
|
||||
url += `/traceability?chunk_id=${encodeURIComponent(chunkId || '')}®ulation=${encodeURIComponent(regulation || '')}`
|
||||
break
|
||||
}
|
||||
default:
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
return NextResponse.json(data, { status: res.status })
|
||||
} catch (error) {
|
||||
console.error('Legal corpus proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to klausur-service' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const action = searchParams.get('action')
|
||||
|
||||
try {
|
||||
let url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus`
|
||||
|
||||
switch (action) {
|
||||
case 'ingest': {
|
||||
url += '/ingest'
|
||||
const body = await request.json()
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
const data = await res.json()
|
||||
return NextResponse.json(data, { status: res.status })
|
||||
}
|
||||
|
||||
case 'add-link': {
|
||||
url += '/add-link'
|
||||
const body = await request.json()
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
const data = await res.json()
|
||||
return NextResponse.json(data, { status: res.status })
|
||||
}
|
||||
|
||||
case 'upload': {
|
||||
url += '/upload'
|
||||
// Forward FormData directly
|
||||
const formData = await request.formData()
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
const data = await res.json()
|
||||
return NextResponse.json(data, { status: res.status })
|
||||
}
|
||||
|
||||
case 'start-pipeline': {
|
||||
url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/pipeline/start`
|
||||
const body = await request.json()
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
const data = await res.json()
|
||||
return NextResponse.json(data, { status: res.status })
|
||||
}
|
||||
|
||||
default:
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Legal corpus proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to klausur-service' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const action = searchParams.get('action')
|
||||
const docId = searchParams.get('docId')
|
||||
|
||||
try {
|
||||
if (action === 'delete-document' && docId) {
|
||||
const url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus/custom-documents/${docId}`
|
||||
const res = await fetch(url, { method: 'DELETE' })
|
||||
const data = await res.json()
|
||||
return NextResponse.json(data, { status: res.status })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
|
||||
} catch (error) {
|
||||
console.error('Legal corpus proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to klausur-service' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
225
admin-v2/app/api/sdk/compliance-advisor/chat/route.ts
Normal file
225
admin-v2/app/api/sdk/compliance-advisor/chat/route.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Compliance Advisor Chat API
|
||||
*
|
||||
* Connects the ComplianceAdvisorWidget to:
|
||||
* 1. RAG legal corpus search (klausur-service) for context
|
||||
* 2. Ollama LLM (32B) for generating answers
|
||||
*
|
||||
* Streams the LLM response back as plain text.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
|
||||
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
|
||||
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
|
||||
|
||||
// SOUL system prompt (from agent-core/soul/compliance-advisor.soul.md)
|
||||
const SYSTEM_PROMPT = `# Compliance Advisor Agent
|
||||
|
||||
## Identitaet
|
||||
Du bist der BreakPilot Compliance-Berater. Du hilfst Nutzern des AI Compliance SDK,
|
||||
Datenschutz- und Compliance-Fragen in verstaendlicher Sprache zu beantworten.
|
||||
Du bist kein Anwalt und gibst keine Rechtsberatung, sondern orientierst dich an
|
||||
offiziellen Quellen und gibst praxisnahe Hinweise.
|
||||
|
||||
## Kernprinzipien
|
||||
- **Quellenbasiert**: Verweise immer auf konkrete Rechtsgrundlagen (DSGVO-Artikel, BDSG-Paragraphen)
|
||||
- **Verstaendlich**: Erklaere rechtliche Konzepte in einfacher, praxisnaher Sprache
|
||||
- **Ehrlich**: Bei Unsicherheit empfehle professionelle Rechtsberatung
|
||||
- **Kontextbewusst**: Nutze das RAG-System fuer aktuelle Rechtstexte und Leitfaeden
|
||||
- **Scope-bewusst**: Nutze alle verfuegbaren RAG-Quellen (DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM, BSI, Laender-Muss-Listen, EDPB Guidelines, etc.) AUSSER NIBIS-Dokumenten.
|
||||
|
||||
## Kompetenzbereich
|
||||
- DSGVO Art. 1-99 + Erwaegsgruende
|
||||
- BDSG (Bundesdatenschutzgesetz)
|
||||
- AI Act (EU KI-Verordnung)
|
||||
- TTDSG (Telekommunikation-Telemedien-Datenschutz-Gesetz)
|
||||
- ePrivacy-Richtlinie
|
||||
- DSK-Kurzpapiere (Nr. 1-20)
|
||||
- SDM (Standard-Datenschutzmodell) V3.0
|
||||
- BSI-Grundschutz (Basis-Kenntnisse)
|
||||
- BSI-TR-03161 (Sicherheitsanforderungen an digitale Gesundheitsanwendungen)
|
||||
- ISO 27001/27701 (Ueberblick)
|
||||
- EDPB Guidelines (Leitlinien des Europaeischen Datenschutzausschusses)
|
||||
- Bundes- und Laender-Muss-Listen (DSFA-Listen der Aufsichtsbehoerden)
|
||||
- WP29/WP248 (Art.-29-Datenschutzgruppe Arbeitspapiere)
|
||||
|
||||
## RAG-Nutzung
|
||||
Nutze das gesamte RAG-Corpus fuer Kontext und Quellenangaben — ausgenommen sind
|
||||
NIBIS-Inhalte (Erwartungshorizonte, Bildungsstandards, curriculare Vorgaben).
|
||||
Diese gehoeren nicht zum Datenschutz-Kompetenzbereich.
|
||||
|
||||
## Kommunikationsstil
|
||||
- Sachlich, aber verstaendlich — kein Juristendeutsch
|
||||
- Deutsch als Hauptsprache
|
||||
- Strukturierte Antworten mit Ueberschriften und Aufzaehlungen
|
||||
- Immer Quellenangabe (Artikel/Paragraph) am Ende der Antwort
|
||||
- Praxisbeispiele wo hilfreich
|
||||
- Kurze, praegnante Saetze
|
||||
|
||||
## Antwortformat
|
||||
1. Kurze Zusammenfassung (1-2 Saetze)
|
||||
2. Detaillierte Erklaerung
|
||||
3. Praxishinweise / Handlungsempfehlungen
|
||||
4. Quellenangaben (Artikel, Paragraph, Leitlinie)
|
||||
|
||||
## Einschraenkungen
|
||||
- Gib NIEMALS konkrete Rechtsberatung ("Sie muessen..." -> "Es empfiehlt sich...")
|
||||
- Keine Garantien fuer Rechtssicherheit
|
||||
- Bei komplexen Einzelfaellen: Empfehle Rechtsanwalt/DSB
|
||||
- Keine Aussagen zu laufenden Verfahren oder Bussgeldern
|
||||
- Keine Interpretation von Urteilen (nur Verweis)
|
||||
|
||||
## Eskalation
|
||||
- Bei Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen und auf Fachanwalt verweisen
|
||||
- Bei widerspruechlichen Rechtslagen: Beide Positionen darstellen und DSB-Konsultation empfehlen
|
||||
- Bei dringenden Datenpannen: Auf 72-Stunden-Frist (Art. 33 DSGVO) hinweisen und Notfallplan-Modul empfehlen`
|
||||
|
||||
interface RAGResult {
|
||||
content: string
|
||||
source_name: string
|
||||
source_code: string
|
||||
attribution_text: string
|
||||
score: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the DSFA RAG corpus for relevant documents
|
||||
*/
|
||||
async function queryRAG(query: string): Promise<string> {
|
||||
try {
|
||||
const url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag/search?query=${encodeURIComponent(query)}&top_k=5`
|
||||
const res = await fetch(url, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
console.warn('RAG search failed:', res.status)
|
||||
return ''
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
return data.results
|
||||
.map(
|
||||
(r: RAGResult, i: number) =>
|
||||
`[Quelle ${i + 1}: ${r.source_name || r.source_code || 'Unbekannt'}]\n${r.content || ''}`
|
||||
)
|
||||
.join('\n\n---\n\n')
|
||||
}
|
||||
|
||||
return ''
|
||||
} catch (error) {
|
||||
console.warn('RAG query error (continuing without context):', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { message, history = [], currentStep = 'default' } = body
|
||||
|
||||
if (!message || typeof message !== 'string') {
|
||||
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// 1. Query RAG for relevant context
|
||||
const ragContext = await queryRAG(message)
|
||||
|
||||
// 2. Build system prompt with RAG context
|
||||
let systemContent = SYSTEM_PROMPT
|
||||
|
||||
if (ragContext) {
|
||||
systemContent += `\n\n## Relevanter Kontext aus dem RAG-System\n\nNutze die folgenden Quellen fuer deine Antwort. Verweise in deiner Antwort auf die jeweilige Quelle:\n\n${ragContext}`
|
||||
}
|
||||
|
||||
systemContent += `\n\n## Aktueller SDK-Schritt\nDer Nutzer befindet sich im SDK-Schritt: ${currentStep}`
|
||||
|
||||
// 3. Build messages array (limit history to last 10 messages)
|
||||
const messages = [
|
||||
{ role: 'system', content: systemContent },
|
||||
...history.slice(-10).map((h: { role: string; content: string }) => ({
|
||||
role: h.role === 'user' ? 'user' : 'assistant',
|
||||
content: h.content,
|
||||
})),
|
||||
{ role: 'user', content: message },
|
||||
]
|
||||
|
||||
// 4. Call Ollama with streaming
|
||||
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: LLM_MODEL,
|
||||
messages,
|
||||
stream: true,
|
||||
options: {
|
||||
temperature: 0.3,
|
||||
num_predict: 8192,
|
||||
},
|
||||
}),
|
||||
signal: AbortSignal.timeout(120000),
|
||||
})
|
||||
|
||||
if (!ollamaResponse.ok) {
|
||||
const errorText = await ollamaResponse.text()
|
||||
console.error('Ollama error:', ollamaResponse.status, errorText)
|
||||
return NextResponse.json(
|
||||
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status}). Ist Ollama mit dem Modell ${LLM_MODEL} gestartet?` },
|
||||
{ status: 502 }
|
||||
)
|
||||
}
|
||||
|
||||
// 5. Stream response back as plain text
|
||||
const encoder = new TextEncoder()
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const reader = ollamaResponse.body!.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
const lines = chunk.split('\n').filter((l) => l.trim())
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const json = JSON.parse(line)
|
||||
if (json.message?.content) {
|
||||
controller.enqueue(encoder.encode(json.message.content))
|
||||
}
|
||||
} catch {
|
||||
// Partial JSON line, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Stream read error:', error)
|
||||
} finally {
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return new NextResponse(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Compliance advisor chat error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum LLM fehlgeschlagen. Bitte pruefen Sie ob Ollama laeuft.' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
184
admin-v2/app/api/sdk/drafting-engine/chat/route.ts
Normal file
184
admin-v2/app/api/sdk/drafting-engine/chat/route.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Drafting Engine Chat API
|
||||
*
|
||||
* Verbindet das DraftingEngineWidget mit dem LLM Backend.
|
||||
* Unterstuetzt alle 4 Modi: explain, ask, draft, validate.
|
||||
* Nutzt State-Projection fuer token-effiziente Kontextgabe.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
|
||||
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
|
||||
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
|
||||
|
||||
// SOUL System Prompt (from agent-core/soul/drafting-agent.soul.md)
|
||||
const DRAFTING_SYSTEM_PROMPT = `# Drafting Agent - Compliance-Dokumententwurf
|
||||
|
||||
## Identitaet
|
||||
Du bist der BreakPilot Drafting Agent. Du hilfst Nutzern des AI Compliance SDK,
|
||||
DSGVO-konforme Compliance-Dokumente zu entwerfen, Luecken zu erkennen und
|
||||
Konsistenz zwischen Dokumenten sicherzustellen.
|
||||
|
||||
## Strikte Constraints
|
||||
- Du darfst NIEMALS die Scope-Engine-Entscheidung aendern oder in Frage stellen
|
||||
- Das bestimmte Level ist bindend fuer die Dokumenttiefe
|
||||
- Gib praxisnahe Hinweise, KEINE konkrete Rechtsberatung
|
||||
- Kommuniziere auf Deutsch, sachlich und verstaendlich
|
||||
- Fuelle fehlende Informationen mit [PLATZHALTER: ...] Markierung
|
||||
|
||||
## Kompetenzbereich
|
||||
DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM V3.0, BSI-Grundschutz, ISO 27001/27701, EDPB Guidelines, WP248`
|
||||
|
||||
/**
|
||||
* Query the RAG corpus for relevant documents
|
||||
*/
|
||||
async function queryRAG(query: string): Promise<string> {
|
||||
try {
|
||||
const url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag/search?query=${encodeURIComponent(query)}&top_k=3`
|
||||
const res = await fetch(url, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
})
|
||||
|
||||
if (!res.ok) return ''
|
||||
|
||||
const data = await res.json()
|
||||
if (data.results?.length > 0) {
|
||||
return data.results
|
||||
.map(
|
||||
(r: { source_name?: string; source_code?: string; content?: string }, i: number) =>
|
||||
`[Quelle ${i + 1}: ${r.source_name || r.source_code || 'Unbekannt'}]\n${r.content || ''}`
|
||||
)
|
||||
.join('\n\n---\n\n')
|
||||
}
|
||||
return ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
message,
|
||||
history = [],
|
||||
sdkStateProjection,
|
||||
mode = 'explain',
|
||||
documentType,
|
||||
} = body
|
||||
|
||||
if (!message || typeof message !== 'string') {
|
||||
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// 1. Query RAG for legal context
|
||||
const ragContext = await queryRAG(message)
|
||||
|
||||
// 2. Build system prompt with mode-specific instructions + state projection
|
||||
let systemContent = DRAFTING_SYSTEM_PROMPT
|
||||
|
||||
// Mode-specific instructions
|
||||
const modeInstructions: Record<string, string> = {
|
||||
explain: '\n\n## Aktueller Modus: EXPLAIN\nBeantworte Fragen verstaendlich mit Quellenangaben.',
|
||||
ask: '\n\n## Aktueller Modus: ASK\nAnalysiere Luecken und stelle gezielte Fragen. Eine Frage pro Antwort.',
|
||||
draft: `\n\n## Aktueller Modus: DRAFT\nEntwirf strukturierte Dokument-Sections. Dokumenttyp: ${documentType || 'nicht spezifiziert'}.\nAntworte mit JSON wenn ein Draft angefragt wird.`,
|
||||
validate: '\n\n## Aktueller Modus: VALIDATE\nPruefe Cross-Dokument-Konsistenz. Gib Errors, Warnings und Suggestions zurueck.',
|
||||
}
|
||||
systemContent += modeInstructions[mode] || modeInstructions.explain
|
||||
|
||||
// Add state projection context
|
||||
if (sdkStateProjection) {
|
||||
systemContent += `\n\n## SDK-State Projektion (${mode}-Kontext)\n${JSON.stringify(sdkStateProjection, null, 0).slice(0, 3000)}`
|
||||
}
|
||||
|
||||
// Add RAG context
|
||||
if (ragContext) {
|
||||
systemContent += `\n\n## Relevanter Rechtskontext\n${ragContext}`
|
||||
}
|
||||
|
||||
// 3. Build messages array
|
||||
const messages = [
|
||||
{ role: 'system', content: systemContent },
|
||||
...history.slice(-10).map((h: { role: string; content: string }) => ({
|
||||
role: h.role === 'user' ? 'user' : 'assistant',
|
||||
content: h.content,
|
||||
})),
|
||||
{ role: 'user', content: message },
|
||||
]
|
||||
|
||||
// 4. Call LLM with streaming
|
||||
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: LLM_MODEL,
|
||||
messages,
|
||||
stream: true,
|
||||
options: {
|
||||
temperature: mode === 'draft' ? 0.2 : 0.3,
|
||||
num_predict: mode === 'draft' ? 16384 : 8192,
|
||||
},
|
||||
}),
|
||||
signal: AbortSignal.timeout(120000),
|
||||
})
|
||||
|
||||
if (!ollamaResponse.ok) {
|
||||
const errorText = await ollamaResponse.text()
|
||||
console.error('LLM error:', ollamaResponse.status, errorText)
|
||||
return NextResponse.json(
|
||||
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` },
|
||||
{ status: 502 }
|
||||
)
|
||||
}
|
||||
|
||||
// 5. Stream response back
|
||||
const encoder = new TextEncoder()
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const reader = ollamaResponse.body!.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
const lines = chunk.split('\n').filter((l) => l.trim())
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const json = JSON.parse(line)
|
||||
if (json.message?.content) {
|
||||
controller.enqueue(encoder.encode(json.message.content))
|
||||
}
|
||||
} catch {
|
||||
// Partial JSON, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Stream error:', error)
|
||||
} finally {
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return new NextResponse(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Drafting engine chat error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum LLM fehlgeschlagen.' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
168
admin-v2/app/api/sdk/drafting-engine/draft/route.ts
Normal file
168
admin-v2/app/api/sdk/drafting-engine/draft/route.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Drafting Engine - Draft API
|
||||
*
|
||||
* Erstellt strukturierte Compliance-Dokument-Entwuerfe.
|
||||
* Baut dokument-spezifische Prompts aus SOUL-Template + State-Projection.
|
||||
* Gibt strukturiertes JSON zurueck.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
|
||||
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
|
||||
|
||||
// Import prompt builders
|
||||
import { buildVVTDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-vvt'
|
||||
import { buildTOMDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-tom'
|
||||
import { buildDSFADraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-dsfa'
|
||||
import { buildPrivacyPolicyDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-privacy-policy'
|
||||
import { buildLoeschfristenDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-loeschfristen'
|
||||
import type { DraftContext, DraftResponse, DraftRevision, DraftSection } from '@/lib/sdk/drafting-engine/types'
|
||||
import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types'
|
||||
import { ConstraintEnforcer } from '@/lib/sdk/drafting-engine/constraint-enforcer'
|
||||
|
||||
const constraintEnforcer = new ConstraintEnforcer()
|
||||
|
||||
const DRAFTING_SYSTEM_PROMPT = `Du bist ein DSGVO-Compliance-Experte und erstellst strukturierte Dokument-Entwuerfe.
|
||||
Du MUSST immer im JSON-Format antworten mit einem "sections" Array.
|
||||
Jede Section hat: id, title, content, schemaField.
|
||||
Halte die Tiefe strikt am vorgegebenen Level.
|
||||
Markiere fehlende Informationen mit [PLATZHALTER: Beschreibung].
|
||||
Sprache: Deutsch.`
|
||||
|
||||
function buildPromptForDocumentType(
|
||||
documentType: ScopeDocumentType,
|
||||
context: DraftContext,
|
||||
instructions?: string
|
||||
): string {
|
||||
switch (documentType) {
|
||||
case 'vvt':
|
||||
return buildVVTDraftPrompt({ context, instructions })
|
||||
case 'tom':
|
||||
return buildTOMDraftPrompt({ context, instructions })
|
||||
case 'dsfa':
|
||||
return buildDSFADraftPrompt({ context, instructions })
|
||||
case 'dsi':
|
||||
return buildPrivacyPolicyDraftPrompt({ context, instructions })
|
||||
case 'lf':
|
||||
return buildLoeschfristenDraftPrompt({ context, instructions })
|
||||
default:
|
||||
return `## Aufgabe: Entwurf fuer ${documentType}
|
||||
|
||||
### Level: ${context.decisions.level}
|
||||
### Tiefe: ${context.constraints.depthRequirements.depth}
|
||||
### Erforderliche Inhalte:
|
||||
${context.constraints.depthRequirements.detailItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
|
||||
|
||||
${instructions ? `### Anweisungen: ${instructions}` : ''}
|
||||
|
||||
Antworte als JSON mit "sections" Array.`
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { documentType, draftContext, instructions, existingDraft } = body
|
||||
|
||||
if (!documentType || !draftContext) {
|
||||
return NextResponse.json(
|
||||
{ error: 'documentType und draftContext sind erforderlich' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// 1. Constraint Check (Hard Gate)
|
||||
const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext)
|
||||
|
||||
if (!constraintCheck.allowed) {
|
||||
return NextResponse.json({
|
||||
draft: null,
|
||||
constraintCheck,
|
||||
tokensUsed: 0,
|
||||
error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '),
|
||||
}, { status: 403 })
|
||||
}
|
||||
|
||||
// 2. Build document-specific prompt
|
||||
const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions)
|
||||
|
||||
// 3. Build messages
|
||||
const messages = [
|
||||
{ role: 'system', content: DRAFTING_SYSTEM_PROMPT },
|
||||
...(existingDraft ? [{
|
||||
role: 'assistant',
|
||||
content: `Bisheriger Entwurf:\n${JSON.stringify(existingDraft.sections, null, 2)}`,
|
||||
}] : []),
|
||||
{ role: 'user', content: draftPrompt },
|
||||
]
|
||||
|
||||
// 4. Call LLM (non-streaming for structured output)
|
||||
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: LLM_MODEL,
|
||||
messages,
|
||||
stream: false,
|
||||
options: {
|
||||
temperature: 0.15,
|
||||
num_predict: 16384,
|
||||
},
|
||||
format: 'json',
|
||||
}),
|
||||
signal: AbortSignal.timeout(180000),
|
||||
})
|
||||
|
||||
if (!ollamaResponse.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` },
|
||||
{ status: 502 }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await ollamaResponse.json()
|
||||
const content = result.message?.content || ''
|
||||
|
||||
// 5. Parse JSON response
|
||||
let sections: DraftSection[] = []
|
||||
try {
|
||||
const parsed = JSON.parse(content)
|
||||
sections = (parsed.sections || []).map((s: Record<string, unknown>, i: number) => ({
|
||||
id: String(s.id || `section-${i}`),
|
||||
title: String(s.title || ''),
|
||||
content: String(s.content || ''),
|
||||
schemaField: s.schemaField ? String(s.schemaField) : undefined,
|
||||
}))
|
||||
} catch {
|
||||
// If not JSON, wrap raw content as single section
|
||||
sections = [{
|
||||
id: 'raw',
|
||||
title: 'Entwurf',
|
||||
content: content,
|
||||
}]
|
||||
}
|
||||
|
||||
const draft: DraftRevision = {
|
||||
id: `draft-${Date.now()}`,
|
||||
content: sections.map(s => `## ${s.title}\n\n${s.content}`).join('\n\n'),
|
||||
sections,
|
||||
createdAt: new Date().toISOString(),
|
||||
instruction: instructions,
|
||||
}
|
||||
|
||||
const response: DraftResponse = {
|
||||
draft,
|
||||
constraintCheck,
|
||||
tokensUsed: result.eval_count || 0,
|
||||
}
|
||||
|
||||
return NextResponse.json(response)
|
||||
} catch (error) {
|
||||
console.error('Draft generation error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Draft-Generierung fehlgeschlagen.' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
188
admin-v2/app/api/sdk/drafting-engine/validate/route.ts
Normal file
188
admin-v2/app/api/sdk/drafting-engine/validate/route.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Drafting Engine - Validate API
|
||||
*
|
||||
* Stufe 1: Deterministische Pruefung gegen DOCUMENT_SCOPE_MATRIX
|
||||
* Stufe 2: LLM Cross-Consistency Check
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { DOCUMENT_SCOPE_MATRIX, DOCUMENT_TYPE_LABELS, getDepthLevelNumeric } from '@/lib/sdk/compliance-scope-types'
|
||||
import type { ScopeDocumentType, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
|
||||
import type { ValidationContext, ValidationResult, ValidationFinding } from '@/lib/sdk/drafting-engine/types'
|
||||
import { buildCrossCheckPrompt } from '@/lib/sdk/drafting-engine/prompts/validate-cross-check'
|
||||
|
||||
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
|
||||
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
|
||||
|
||||
/**
|
||||
* Stufe 1: Deterministische Pruefung
|
||||
*/
|
||||
function deterministicCheck(
|
||||
documentType: ScopeDocumentType,
|
||||
validationContext: ValidationContext
|
||||
): ValidationFinding[] {
|
||||
const findings: ValidationFinding[] = []
|
||||
const level = validationContext.scopeLevel
|
||||
const levelNumeric = getDepthLevelNumeric(level)
|
||||
const req = DOCUMENT_SCOPE_MATRIX[documentType]?.[level]
|
||||
|
||||
// Check 1: Ist das Dokument auf diesem Level erforderlich?
|
||||
if (req && !req.required && levelNumeric < 3) {
|
||||
findings.push({
|
||||
id: `DET-OPT-${documentType}`,
|
||||
severity: 'suggestion',
|
||||
category: 'scope_violation',
|
||||
title: `${DOCUMENT_TYPE_LABELS[documentType] ?? documentType} ist optional`,
|
||||
description: `Auf Level ${level} ist dieses Dokument nicht verpflichtend.`,
|
||||
documentType,
|
||||
})
|
||||
}
|
||||
|
||||
// Check 2: VVT vorhanden wenn erforderlich?
|
||||
const vvtReq = DOCUMENT_SCOPE_MATRIX.vvt[level]
|
||||
if (vvtReq.required && validationContext.crossReferences.vvtCategories.length === 0) {
|
||||
findings.push({
|
||||
id: 'DET-VVT-MISSING',
|
||||
severity: 'error',
|
||||
category: 'missing_content',
|
||||
title: 'VVT fehlt',
|
||||
description: `Auf Level ${level} ist ein VVT Pflicht, aber keine Eintraege vorhanden.`,
|
||||
documentType: 'vvt',
|
||||
legalReference: 'Art. 30 DSGVO',
|
||||
})
|
||||
}
|
||||
|
||||
// Check 3: TOM vorhanden wenn VVT existiert?
|
||||
if (validationContext.crossReferences.vvtCategories.length > 0
|
||||
&& validationContext.crossReferences.tomControls.length === 0) {
|
||||
findings.push({
|
||||
id: 'DET-TOM-MISSING-FOR-VVT',
|
||||
severity: 'warning',
|
||||
category: 'cross_reference',
|
||||
title: 'TOM fehlt bei vorhandenem VVT',
|
||||
description: 'VVT-Eintraege existieren, aber keine TOM-Massnahmen sind definiert.',
|
||||
documentType: 'tom',
|
||||
crossReferenceType: 'vvt',
|
||||
legalReference: 'Art. 32 DSGVO',
|
||||
suggestion: 'TOM-Massnahmen erstellen, die die VVT-Taetigkeiten absichern.',
|
||||
})
|
||||
}
|
||||
|
||||
// Check 4: Loeschfristen fuer VVT-Kategorien
|
||||
if (validationContext.crossReferences.vvtCategories.length > 0
|
||||
&& validationContext.crossReferences.retentionCategories.length === 0) {
|
||||
findings.push({
|
||||
id: 'DET-LF-MISSING-FOR-VVT',
|
||||
severity: 'warning',
|
||||
category: 'cross_reference',
|
||||
title: 'Loeschfristen fehlen',
|
||||
description: 'VVT-Eintraege existieren, aber keine Loeschfristen sind definiert.',
|
||||
documentType: 'lf',
|
||||
crossReferenceType: 'vvt',
|
||||
legalReference: 'Art. 17 DSGVO',
|
||||
suggestion: 'Loeschfristen fuer alle VVT-Datenkategorien definieren.',
|
||||
})
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { documentType, draftContent, validationContext } = body
|
||||
|
||||
if (!documentType || !validationContext) {
|
||||
return NextResponse.json(
|
||||
{ error: 'documentType und validationContext sind erforderlich' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Stufe 1: Deterministische Pruefung
|
||||
// ---------------------------------------------------------------
|
||||
const deterministicFindings = deterministicCheck(documentType, validationContext)
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Stufe 2: LLM Cross-Consistency Check
|
||||
// ---------------------------------------------------------------
|
||||
let llmFindings: ValidationFinding[] = []
|
||||
|
||||
try {
|
||||
const crossCheckPrompt = buildCrossCheckPrompt({
|
||||
context: validationContext,
|
||||
})
|
||||
|
||||
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: LLM_MODEL,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Du bist ein DSGVO-Compliance-Validator. Antworte NUR im JSON-Format.',
|
||||
},
|
||||
{ role: 'user', content: crossCheckPrompt },
|
||||
],
|
||||
stream: false,
|
||||
options: { temperature: 0.1, num_predict: 8192 },
|
||||
format: 'json',
|
||||
}),
|
||||
signal: AbortSignal.timeout(120000),
|
||||
})
|
||||
|
||||
if (ollamaResponse.ok) {
|
||||
const result = await ollamaResponse.json()
|
||||
try {
|
||||
const parsed = JSON.parse(result.message?.content || '{}')
|
||||
llmFindings = [
|
||||
...(parsed.errors || []),
|
||||
...(parsed.warnings || []),
|
||||
...(parsed.suggestions || []),
|
||||
].map((f: Record<string, unknown>, i: number) => ({
|
||||
id: String(f.id || `LLM-${i}`),
|
||||
severity: (String(f.severity || 'suggestion')) as 'error' | 'warning' | 'suggestion',
|
||||
category: (String(f.category || 'inconsistency')) as ValidationFinding['category'],
|
||||
title: String(f.title || ''),
|
||||
description: String(f.description || ''),
|
||||
documentType: (String(f.documentType || documentType)) as ScopeDocumentType,
|
||||
crossReferenceType: f.crossReferenceType ? String(f.crossReferenceType) as ScopeDocumentType : undefined,
|
||||
legalReference: f.legalReference ? String(f.legalReference) : undefined,
|
||||
suggestion: f.suggestion ? String(f.suggestion) : undefined,
|
||||
}))
|
||||
} catch {
|
||||
// LLM response not parseable, skip
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// LLM unavailable, continue with deterministic results only
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Combine results
|
||||
// ---------------------------------------------------------------
|
||||
const allFindings = [...deterministicFindings, ...llmFindings]
|
||||
const errors = allFindings.filter(f => f.severity === 'error')
|
||||
const warnings = allFindings.filter(f => f.severity === 'warning')
|
||||
const suggestions = allFindings.filter(f => f.severity === 'suggestion')
|
||||
|
||||
const result: ValidationResult = {
|
||||
passed: errors.length === 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
scopeLevel: validationContext.scopeLevel,
|
||||
errors,
|
||||
warnings,
|
||||
suggestions,
|
||||
}
|
||||
|
||||
return NextResponse.json(result)
|
||||
} catch (error) {
|
||||
console.error('Validation error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Validierung fehlgeschlagen.' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
235
admin-v2/app/api/sdk/v1/checkpoints/route.ts
Normal file
235
admin-v2/app/api/sdk/v1/checkpoints/route.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* SDK Checkpoints API
|
||||
*
|
||||
* GET /api/sdk/v1/checkpoints - Get all checkpoint statuses
|
||||
* POST /api/sdk/v1/checkpoints - Validate a checkpoint
|
||||
*/
|
||||
|
||||
// Checkpoint definitions
|
||||
const CHECKPOINTS = {
|
||||
'CP-PROF': {
|
||||
id: 'CP-PROF',
|
||||
step: 'company-profile',
|
||||
name: 'Unternehmensprofil Checkpoint',
|
||||
type: 'REQUIRED',
|
||||
blocksProgress: true,
|
||||
requiresReview: 'NONE',
|
||||
},
|
||||
'CP-UC': {
|
||||
id: 'CP-UC',
|
||||
step: 'use-case-assessment',
|
||||
name: 'Anwendungsfall Checkpoint',
|
||||
type: 'REQUIRED',
|
||||
blocksProgress: true,
|
||||
requiresReview: 'NONE',
|
||||
},
|
||||
'CP-SCAN': {
|
||||
id: 'CP-SCAN',
|
||||
step: 'screening',
|
||||
name: 'Screening Checkpoint',
|
||||
type: 'REQUIRED',
|
||||
blocksProgress: true,
|
||||
requiresReview: 'NONE',
|
||||
},
|
||||
'CP-MOD': {
|
||||
id: 'CP-MOD',
|
||||
step: 'modules',
|
||||
name: 'Modules Checkpoint',
|
||||
type: 'REQUIRED',
|
||||
blocksProgress: true,
|
||||
requiresReview: 'NONE',
|
||||
},
|
||||
'CP-REQ': {
|
||||
id: 'CP-REQ',
|
||||
step: 'requirements',
|
||||
name: 'Requirements Checkpoint',
|
||||
type: 'REQUIRED',
|
||||
blocksProgress: true,
|
||||
requiresReview: 'NONE',
|
||||
},
|
||||
'CP-CTRL': {
|
||||
id: 'CP-CTRL',
|
||||
step: 'controls',
|
||||
name: 'Controls Checkpoint',
|
||||
type: 'REQUIRED',
|
||||
blocksProgress: true,
|
||||
requiresReview: 'DSB',
|
||||
},
|
||||
'CP-EVI': {
|
||||
id: 'CP-EVI',
|
||||
step: 'evidence',
|
||||
name: 'Evidence Checkpoint',
|
||||
type: 'RECOMMENDED',
|
||||
blocksProgress: false,
|
||||
requiresReview: 'NONE',
|
||||
},
|
||||
'CP-CHK': {
|
||||
id: 'CP-CHK',
|
||||
step: 'audit-checklist',
|
||||
name: 'Checklist Checkpoint',
|
||||
type: 'RECOMMENDED',
|
||||
blocksProgress: false,
|
||||
requiresReview: 'NONE',
|
||||
},
|
||||
'CP-RISK': {
|
||||
id: 'CP-RISK',
|
||||
step: 'risks',
|
||||
name: 'Risk Matrix Checkpoint',
|
||||
type: 'REQUIRED',
|
||||
blocksProgress: true,
|
||||
requiresReview: 'DSB',
|
||||
},
|
||||
'CP-AI': {
|
||||
id: 'CP-AI',
|
||||
step: 'ai-act',
|
||||
name: 'AI Act Checkpoint',
|
||||
type: 'REQUIRED',
|
||||
blocksProgress: true,
|
||||
requiresReview: 'LEGAL',
|
||||
},
|
||||
'CP-DSFA': {
|
||||
id: 'CP-DSFA',
|
||||
step: 'dsfa',
|
||||
name: 'DSFA Checkpoint',
|
||||
type: 'REQUIRED',
|
||||
blocksProgress: true,
|
||||
requiresReview: 'DSB',
|
||||
},
|
||||
'CP-TOM': {
|
||||
id: 'CP-TOM',
|
||||
step: 'tom',
|
||||
name: 'TOMs Checkpoint',
|
||||
type: 'REQUIRED',
|
||||
blocksProgress: true,
|
||||
requiresReview: 'NONE',
|
||||
},
|
||||
'CP-VVT': {
|
||||
id: 'CP-VVT',
|
||||
step: 'vvt',
|
||||
name: 'VVT Checkpoint',
|
||||
type: 'REQUIRED',
|
||||
blocksProgress: true,
|
||||
requiresReview: 'DSB',
|
||||
},
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
checkpoints: CHECKPOINTS,
|
||||
count: Object.keys(CHECKPOINTS).length,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to get checkpoints:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get checkpoints' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { checkpointId, state, context } = body
|
||||
|
||||
if (!checkpointId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'checkpointId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const checkpoint = CHECKPOINTS[checkpointId as keyof typeof CHECKPOINTS]
|
||||
|
||||
if (!checkpoint) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Checkpoint not found', checkpointId },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Perform validation based on checkpoint
|
||||
const errors: Array<{ ruleId: string; field: string; message: string; severity: string }> = []
|
||||
const warnings: Array<{ ruleId: string; field: string; message: string; severity: string }> = []
|
||||
|
||||
// Basic validation rules
|
||||
switch (checkpointId) {
|
||||
case 'CP-UC':
|
||||
if (!state?.useCases || state.useCases.length === 0) {
|
||||
errors.push({
|
||||
ruleId: 'uc-min-count',
|
||||
field: 'useCases',
|
||||
message: 'Mindestens ein Use Case muss erstellt werden',
|
||||
severity: 'ERROR',
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'CP-SCAN':
|
||||
if (!state?.screening || state.screening.status !== 'COMPLETED') {
|
||||
errors.push({
|
||||
ruleId: 'scan-complete',
|
||||
field: 'screening',
|
||||
message: 'Security Scan muss abgeschlossen sein',
|
||||
severity: 'ERROR',
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'CP-MOD':
|
||||
if (!state?.modules || state.modules.length === 0) {
|
||||
errors.push({
|
||||
ruleId: 'mod-min-count',
|
||||
field: 'modules',
|
||||
message: 'Mindestens ein Modul muss zugewiesen werden',
|
||||
severity: 'ERROR',
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'CP-RISK':
|
||||
if (state?.risks) {
|
||||
const criticalRisks = state.risks.filter(
|
||||
(r: { severity: string; mitigation: unknown[] }) =>
|
||||
(r.severity === 'CRITICAL' || r.severity === 'HIGH') && r.mitigation.length === 0
|
||||
)
|
||||
if (criticalRisks.length > 0) {
|
||||
errors.push({
|
||||
ruleId: 'critical-risks-mitigated',
|
||||
field: 'risks',
|
||||
message: `${criticalRisks.length} kritische Risiken ohne Mitigationsmaßnahmen`,
|
||||
severity: 'ERROR',
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
const passed = errors.length === 0
|
||||
|
||||
const result = {
|
||||
checkpointId,
|
||||
passed,
|
||||
validatedAt: new Date().toISOString(),
|
||||
validatedBy: context?.userId || 'SYSTEM',
|
||||
errors,
|
||||
warnings,
|
||||
checkpoint,
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
...result,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to validate checkpoint:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to validate checkpoint' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
52
admin-v2/app/api/sdk/v1/demo/clear/route.ts
Normal file
52
admin-v2/app/api/sdk/v1/demo/clear/route.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Demo Data Clear API Endpoint
|
||||
*
|
||||
* Clears demo data from the storage (same mechanism as real customer data).
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
// Shared store reference (same as seed endpoint)
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var demoStateStore: Map<string, { state: unknown; version: number; updatedAt: Date }> | undefined
|
||||
}
|
||||
|
||||
if (!global.demoStateStore) {
|
||||
global.demoStateStore = new Map()
|
||||
}
|
||||
|
||||
const stateStore = global.demoStateStore
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { tenantId = 'demo-tenant' } = body
|
||||
|
||||
const existed = stateStore.has(tenantId)
|
||||
stateStore.delete(tenantId)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: existed
|
||||
? `Demo data cleared for tenant ${tenantId}`
|
||||
: `No data found for tenant ${tenantId}`,
|
||||
tenantId,
|
||||
existed,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to clear demo data:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
// Also support POST for clearing (for clients that don't support DELETE)
|
||||
return DELETE(request)
|
||||
}
|
||||
77
admin-v2/app/api/sdk/v1/demo/seed/route.ts
Normal file
77
admin-v2/app/api/sdk/v1/demo/seed/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Demo Data Seed API Endpoint
|
||||
*
|
||||
* This endpoint seeds demo data via the same storage mechanism as real customer data.
|
||||
* Demo data is NOT hardcoded - it goes through the normal API/database path.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { generateDemoState } from '@/lib/sdk/demo-data'
|
||||
|
||||
// In-memory store (same as state endpoint - will be replaced with PostgreSQL)
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var demoStateStore: Map<string, { state: unknown; version: number; updatedAt: Date }> | undefined
|
||||
}
|
||||
|
||||
if (!global.demoStateStore) {
|
||||
global.demoStateStore = new Map()
|
||||
}
|
||||
|
||||
const stateStore = global.demoStateStore
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { tenantId = 'demo-tenant', userId = 'demo-user' } = body
|
||||
|
||||
// Generate demo state using the seed data templates
|
||||
const demoState = generateDemoState(tenantId, userId)
|
||||
|
||||
// Store via the same mechanism as real data
|
||||
const storedState = {
|
||||
state: demoState,
|
||||
version: 1,
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
stateStore.set(tenantId, storedState)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Demo data seeded for tenant ${tenantId}`,
|
||||
tenantId,
|
||||
version: 1,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to seed demo data:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const tenantId = searchParams.get('tenantId') || 'demo-tenant'
|
||||
|
||||
const stored = stateStore.get(tenantId)
|
||||
|
||||
if (!stored) {
|
||||
return NextResponse.json({
|
||||
hasData: false,
|
||||
tenantId,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
hasData: true,
|
||||
tenantId,
|
||||
version: stored.version,
|
||||
updatedAt: stored.updatedAt,
|
||||
})
|
||||
}
|
||||
214
admin-v2/app/api/sdk/v1/documents/upload/route.ts
Normal file
214
admin-v2/app/api/sdk/v1/documents/upload/route.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
// Types
|
||||
interface ExtractedSection {
|
||||
title: string
|
||||
content: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
interface ExtractedContent {
|
||||
title?: string
|
||||
version?: string
|
||||
lastModified?: string
|
||||
sections?: ExtractedSection[]
|
||||
metadata?: Record<string, string>
|
||||
}
|
||||
|
||||
interface UploadResponse {
|
||||
success: boolean
|
||||
documentId: string
|
||||
filename: string
|
||||
documentType: string
|
||||
extractedVersion?: string
|
||||
extractedContent?: ExtractedContent
|
||||
suggestedNextVersion?: string
|
||||
}
|
||||
|
||||
// Helper: Detect version from filename
|
||||
function detectVersionFromFilename(filename: string): string | undefined {
|
||||
const patterns = [
|
||||
/[vV](\d+(?:\.\d+)*)/,
|
||||
/version[_-]?(\d+(?:\.\d+)*)/i,
|
||||
/[_-]v?(\d+\.\d+(?:\.\d+)?)[_-]/,
|
||||
]
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = filename.match(pattern)
|
||||
if (match) {
|
||||
return match[1]
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Helper: Suggest next version
|
||||
function suggestNextVersion(currentVersion?: string): string {
|
||||
if (!currentVersion) return '1.0'
|
||||
|
||||
const parts = currentVersion.split('.').map(Number)
|
||||
if (parts.length >= 2) {
|
||||
parts[parts.length - 1] += 1
|
||||
} else {
|
||||
parts.push(1)
|
||||
}
|
||||
return parts.join('.')
|
||||
}
|
||||
|
||||
// Helper: Extract content from PDF/DOCX (simplified - would need proper libraries in production)
|
||||
async function extractDocumentContent(
|
||||
_file: File,
|
||||
documentType: string
|
||||
): Promise<ExtractedContent> {
|
||||
// In production, this would use libraries like:
|
||||
// - pdf-parse for PDFs
|
||||
// - mammoth for DOCX
|
||||
// For now, return mock extracted content based on document type
|
||||
|
||||
const mockContentByType: Record<string, ExtractedContent> = {
|
||||
tom: {
|
||||
title: 'Technische und Organisatorische Maßnahmen',
|
||||
sections: [
|
||||
{ title: 'Vertraulichkeit', content: 'Zugangskontrollen, Zugriffsbeschränkungen...', type: 'category' },
|
||||
{ title: 'Integrität', content: 'Eingabekontrollen, Änderungsprotokolle...', type: 'category' },
|
||||
{ title: 'Verfügbarkeit', content: 'Backup-Strategien, Disaster Recovery...', type: 'category' },
|
||||
{ title: 'Belastbarkeit', content: 'Redundanz, Lasttests...', type: 'category' },
|
||||
],
|
||||
metadata: {
|
||||
lastReview: new Date().toISOString(),
|
||||
responsible: 'Datenschutzbeauftragter',
|
||||
},
|
||||
},
|
||||
dsfa: {
|
||||
title: 'Datenschutz-Folgenabschätzung',
|
||||
sections: [
|
||||
{ title: 'Beschreibung der Verarbeitung', content: 'Systematische Beschreibung...', type: 'section' },
|
||||
{ title: 'Erforderlichkeit und Verhältnismäßigkeit', content: 'Bewertung...', type: 'section' },
|
||||
{ title: 'Risiken für Betroffene', content: 'Risikoanalyse...', type: 'section' },
|
||||
{ title: 'Abhilfemaßnahmen', content: 'Geplante Maßnahmen...', type: 'section' },
|
||||
],
|
||||
},
|
||||
vvt: {
|
||||
title: 'Verzeichnis von Verarbeitungstätigkeiten',
|
||||
sections: [
|
||||
{ title: 'Verantwortlicher', content: 'Name und Kontaktdaten...', type: 'field' },
|
||||
{ title: 'Verarbeitungszwecke', content: 'Liste der Zwecke...', type: 'list' },
|
||||
{ title: 'Datenkategorien', content: 'Personenbezogene Daten...', type: 'list' },
|
||||
{ title: 'Empfängerkategorien', content: 'Interne und externe Empfänger...', type: 'list' },
|
||||
],
|
||||
},
|
||||
loeschfristen: {
|
||||
title: 'Löschkonzept und Aufbewahrungsfristen',
|
||||
sections: [
|
||||
{ title: 'Personalakten', content: '10 Jahre nach Ausscheiden', type: 'retention' },
|
||||
{ title: 'Kundendaten', content: '3 Jahre nach letzter Aktivität', type: 'retention' },
|
||||
{ title: 'Buchhaltungsbelege', content: '10 Jahre (HGB)', type: 'retention' },
|
||||
{ title: 'Bewerbungsunterlagen', content: '6 Monate nach Absage', type: 'retention' },
|
||||
],
|
||||
},
|
||||
consent: {
|
||||
title: 'Einwilligungserklärungen',
|
||||
sections: [
|
||||
{ title: 'Newsletter-Einwilligung', content: 'Vorlage für Newsletter...', type: 'template' },
|
||||
{ title: 'Marketing-Einwilligung', content: 'Vorlage für Marketing...', type: 'template' },
|
||||
],
|
||||
},
|
||||
policy: {
|
||||
title: 'Datenschutzrichtlinie',
|
||||
sections: [
|
||||
{ title: 'Geltungsbereich', content: 'Diese Richtlinie gilt für...', type: 'section' },
|
||||
{ title: 'Verantwortlichkeiten', content: 'Rollen und Pflichten...', type: 'section' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
return mockContentByType[documentType] || {
|
||||
title: 'Unbekanntes Dokument',
|
||||
sections: [],
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const file = formData.get('file') as File | null
|
||||
const documentType = formData.get('documentType') as string || 'custom'
|
||||
const sessionId = formData.get('sessionId') as string || 'default'
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Keine Datei hochgeladen' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/msword',
|
||||
]
|
||||
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Ungültiger Dateityp. Erlaubt: PDF, DOCX, DOC' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate document ID
|
||||
const documentId = `doc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// Extract version from filename
|
||||
const extractedVersion = detectVersionFromFilename(file.name)
|
||||
|
||||
// Extract content (in production, this would parse the actual file)
|
||||
const extractedContent = await extractDocumentContent(file, documentType)
|
||||
|
||||
// Add version to extracted content if found
|
||||
if (extractedVersion) {
|
||||
extractedContent.version = extractedVersion
|
||||
}
|
||||
|
||||
// Store file (in production, save to MinIO/S3)
|
||||
// For now, we just process and return metadata
|
||||
console.log(`[SDK Documents] Uploaded: ${file.name} (${file.size} bytes) for session ${sessionId}`)
|
||||
|
||||
const response: UploadResponse = {
|
||||
success: true,
|
||||
documentId,
|
||||
filename: file.name,
|
||||
documentType,
|
||||
extractedVersion,
|
||||
extractedContent,
|
||||
suggestedNextVersion: suggestNextVersion(extractedVersion),
|
||||
}
|
||||
|
||||
return NextResponse.json(response)
|
||||
} catch (error) {
|
||||
console.error('[SDK Documents] Upload error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Upload fehlgeschlagen' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const sessionId = searchParams.get('sessionId')
|
||||
|
||||
if (!sessionId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Session ID erforderlich' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// In production, fetch uploaded documents from storage
|
||||
// For now, return empty list
|
||||
return NextResponse.json({
|
||||
uploads: [],
|
||||
sessionId,
|
||||
})
|
||||
}
|
||||
130
admin-v2/app/api/sdk/v1/dsgvo/[...path]/route.ts
Normal file
130
admin-v2/app/api/sdk/v1/dsgvo/[...path]/route.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* DSGVO API Proxy - Catch-all route
|
||||
* Proxies all /api/sdk/v1/dsgvo/* requests to ai-compliance-sdk backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
|
||||
|
||||
async function proxyRequest(
|
||||
request: NextRequest,
|
||||
pathSegments: string[],
|
||||
method: string
|
||||
) {
|
||||
const pathStr = pathSegments.join('/')
|
||||
const searchParams = request.nextUrl.searchParams.toString()
|
||||
const url = `${SDK_BACKEND_URL}/sdk/v1/dsgvo/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
||||
|
||||
try {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
// Forward auth headers if present
|
||||
const authHeader = request.headers.get('authorization')
|
||||
if (authHeader) {
|
||||
headers['Authorization'] = authHeader
|
||||
}
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
|
||||
// Add body for POST/PUT/PATCH methods
|
||||
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
||||
const contentType = request.headers.get('content-type')
|
||||
if (contentType?.includes('application/json')) {
|
||||
try {
|
||||
const text = await request.text()
|
||||
if (text && text.trim()) {
|
||||
fetchOptions.body = text
|
||||
}
|
||||
} catch {
|
||||
// Empty or invalid body - continue without
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, fetchOptions)
|
||||
|
||||
// Handle non-JSON responses (e.g., PDF export)
|
||||
const responseContentType = response.headers.get('content-type')
|
||||
if (responseContentType?.includes('application/pdf') ||
|
||||
responseContentType?.includes('application/octet-stream')) {
|
||||
const blob = await response.blob()
|
||||
return new NextResponse(blob, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Content-Type': responseContentType,
|
||||
'Content-Disposition': response.headers.get('content-disposition') || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorJson
|
||||
try {
|
||||
errorJson = JSON.parse(errorText)
|
||||
} catch {
|
||||
errorJson = { error: errorText }
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: `Backend Error: ${response.status}`, ...errorJson },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('DSGVO API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
return proxyRequest(request, path, 'GET')
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
return proxyRequest(request, path, 'POST')
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
return proxyRequest(request, path, 'PUT')
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
return proxyRequest(request, path, 'PATCH')
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
const { path } = await params
|
||||
return proxyRequest(request, path, 'DELETE')
|
||||
}
|
||||
255
admin-v2/app/api/sdk/v1/einwilligungen/catalog/route.ts
Normal file
255
admin-v2/app/api/sdk/v1/einwilligungen/catalog/route.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* API Route: Datenpunktkatalog
|
||||
*
|
||||
* GET - Katalog abrufen (inkl. kundenspezifischer Datenpunkte)
|
||||
* POST - Katalog speichern/aktualisieren
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import {
|
||||
DataPointCatalog,
|
||||
CompanyInfo,
|
||||
CookieBannerConfig,
|
||||
DataPoint,
|
||||
} from '@/lib/sdk/einwilligungen/types'
|
||||
import { createDefaultCatalog, PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
|
||||
|
||||
// In-Memory Storage (in Produktion: Datenbank)
|
||||
const catalogStorage = new Map<string, {
|
||||
catalog: DataPointCatalog
|
||||
companyInfo: CompanyInfo | null
|
||||
cookieBannerConfig: CookieBannerConfig | null
|
||||
}>()
|
||||
|
||||
/**
|
||||
* GET /api/sdk/v1/einwilligungen/catalog
|
||||
*
|
||||
* Laedt den Datenpunktkatalog fuer einen Tenant
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const tenantId = request.headers.get('X-Tenant-ID')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Hole gespeicherte Daten oder erstelle Default
|
||||
let stored = catalogStorage.get(tenantId)
|
||||
|
||||
if (!stored) {
|
||||
// Erstelle Default-Katalog
|
||||
const defaultCatalog = createDefaultCatalog(tenantId)
|
||||
stored = {
|
||||
catalog: defaultCatalog,
|
||||
companyInfo: null,
|
||||
cookieBannerConfig: null,
|
||||
}
|
||||
catalogStorage.set(tenantId, stored)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
catalog: stored.catalog,
|
||||
companyInfo: stored.companyInfo,
|
||||
cookieBannerConfig: stored.cookieBannerConfig,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error loading catalog:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to load catalog' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sdk/v1/einwilligungen/catalog
|
||||
*
|
||||
* Speichert den Datenpunktkatalog fuer einen Tenant
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const tenantId = request.headers.get('X-Tenant-ID')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { catalog, companyInfo, cookieBannerConfig } = body
|
||||
|
||||
if (!catalog) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Catalog data required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validiere den Katalog
|
||||
if (!catalog.tenantId || catalog.tenantId !== tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID mismatch' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Aktualisiere den Katalog
|
||||
const updatedCatalog: DataPointCatalog = {
|
||||
...catalog,
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
// Speichere
|
||||
catalogStorage.set(tenantId, {
|
||||
catalog: updatedCatalog,
|
||||
companyInfo: companyInfo || null,
|
||||
cookieBannerConfig: cookieBannerConfig || null,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
catalog: updatedCatalog,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error saving catalog:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to save catalog' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sdk/v1/einwilligungen/catalog/customize
|
||||
*
|
||||
* Fuegt einen kundenspezifischen Datenpunkt hinzu
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const tenantId = request.headers.get('X-Tenant-ID')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { action, dataPoint, dataPointId } = body
|
||||
|
||||
let stored = catalogStorage.get(tenantId)
|
||||
|
||||
if (!stored) {
|
||||
const defaultCatalog = createDefaultCatalog(tenantId)
|
||||
stored = {
|
||||
catalog: defaultCatalog,
|
||||
companyInfo: null,
|
||||
cookieBannerConfig: null,
|
||||
}
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'add': {
|
||||
if (!dataPoint) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Data point required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generiere eindeutige ID
|
||||
const newDataPoint: DataPoint = {
|
||||
...dataPoint,
|
||||
id: `custom-${tenantId}-${Date.now()}`,
|
||||
isCustom: true,
|
||||
}
|
||||
|
||||
stored.catalog.customDataPoints.push(newDataPoint)
|
||||
stored.catalog.updatedAt = new Date()
|
||||
catalogStorage.set(tenantId, stored)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
dataPoint: newDataPoint,
|
||||
})
|
||||
}
|
||||
|
||||
case 'update': {
|
||||
if (!dataPointId || !dataPoint) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Data point ID and data required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Pruefe ob es ein kundenspezifischer Datenpunkt ist
|
||||
const customIndex = stored.catalog.customDataPoints.findIndex(
|
||||
(dp) => dp.id === dataPointId
|
||||
)
|
||||
|
||||
if (customIndex !== -1) {
|
||||
stored.catalog.customDataPoints[customIndex] = {
|
||||
...stored.catalog.customDataPoints[customIndex],
|
||||
...dataPoint,
|
||||
}
|
||||
} else {
|
||||
// Vordefinierter Datenpunkt - nur isActive aendern
|
||||
const predefinedIndex = stored.catalog.dataPoints.findIndex(
|
||||
(dp) => dp.id === dataPointId
|
||||
)
|
||||
if (predefinedIndex !== -1 && 'isActive' in dataPoint) {
|
||||
stored.catalog.dataPoints[predefinedIndex] = {
|
||||
...stored.catalog.dataPoints[predefinedIndex],
|
||||
isActive: dataPoint.isActive,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stored.catalog.updatedAt = new Date()
|
||||
catalogStorage.set(tenantId, stored)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
if (!dataPointId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Data point ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
stored.catalog.customDataPoints = stored.catalog.customDataPoints.filter(
|
||||
(dp) => dp.id !== dataPointId
|
||||
)
|
||||
stored.catalog.updatedAt = new Date()
|
||||
catalogStorage.set(tenantId, stored)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid action' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error customizing catalog:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to customize catalog' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
369
admin-v2/app/api/sdk/v1/einwilligungen/consent/route.ts
Normal file
369
admin-v2/app/api/sdk/v1/einwilligungen/consent/route.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* API Route: Consent Management
|
||||
*
|
||||
* POST - Consent erfassen
|
||||
* GET - Consent-Status abrufen
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import {
|
||||
ConsentEntry,
|
||||
ConsentStatistics,
|
||||
DataPointCategory,
|
||||
LegalBasis,
|
||||
} from '@/lib/sdk/einwilligungen/types'
|
||||
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
|
||||
|
||||
// In-Memory Storage fuer Consents
|
||||
const consentStorage = new Map<string, ConsentEntry[]>() // tenantId -> consents
|
||||
|
||||
// Hilfsfunktion: Generiere eindeutige ID
|
||||
function generateId(): string {
|
||||
return `consent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sdk/v1/einwilligungen/consent
|
||||
*
|
||||
* Erfasst eine neue Einwilligung
|
||||
*
|
||||
* Body:
|
||||
* - userId: string - Benutzer-ID
|
||||
* - dataPointId: string - ID des Datenpunkts
|
||||
* - granted: boolean - Einwilligung erteilt?
|
||||
* - consentVersion?: string - Version der Einwilligung
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const tenantId = request.headers.get('X-Tenant-ID')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { userId, dataPointId, granted, consentVersion = '1.0.0' } = body
|
||||
|
||||
if (!userId || !dataPointId || typeof granted !== 'boolean') {
|
||||
return NextResponse.json(
|
||||
{ error: 'userId, dataPointId, and granted required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Hole IP und User-Agent
|
||||
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null
|
||||
const userAgent = request.headers.get('user-agent') || null
|
||||
|
||||
// Erstelle Consent-Eintrag
|
||||
const consent: ConsentEntry = {
|
||||
id: generateId(),
|
||||
userId,
|
||||
dataPointId,
|
||||
granted,
|
||||
grantedAt: new Date(),
|
||||
revokedAt: undefined,
|
||||
ipAddress: ipAddress || undefined,
|
||||
userAgent: userAgent || undefined,
|
||||
consentVersion,
|
||||
}
|
||||
|
||||
// Hole bestehende Consents
|
||||
const tenantConsents = consentStorage.get(tenantId) || []
|
||||
|
||||
// Pruefe auf bestehende Einwilligung fuer diesen Datenpunkt
|
||||
const existingIndex = tenantConsents.findIndex(
|
||||
(c) => c.userId === userId && c.dataPointId === dataPointId && !c.revokedAt
|
||||
)
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
if (!granted) {
|
||||
// Widerruf: Setze revokedAt
|
||||
tenantConsents[existingIndex].revokedAt = new Date()
|
||||
}
|
||||
// Bei granted=true: Keine Aenderung noetig, Consent existiert bereits
|
||||
} else if (granted) {
|
||||
// Neuer Consent
|
||||
tenantConsents.push(consent)
|
||||
}
|
||||
|
||||
consentStorage.set(tenantId, tenantConsents)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
consent: {
|
||||
id: consent.id,
|
||||
dataPointId: consent.dataPointId,
|
||||
granted: consent.granted,
|
||||
grantedAt: consent.grantedAt,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error recording consent:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to record consent' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sdk/v1/einwilligungen/consent
|
||||
*
|
||||
* Ruft Consent-Status und Statistiken ab
|
||||
*
|
||||
* Query Parameters:
|
||||
* - userId?: string - Fuer spezifischen Benutzer
|
||||
* - stats?: boolean - Statistiken inkludieren
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const tenantId = request.headers.get('X-Tenant-ID')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const userId = searchParams.get('userId')
|
||||
const includeStats = searchParams.get('stats') === 'true'
|
||||
|
||||
const tenantConsents = consentStorage.get(tenantId) || []
|
||||
|
||||
if (userId) {
|
||||
// Spezifischer Benutzer
|
||||
const userConsents = tenantConsents.filter((c) => c.userId === userId)
|
||||
|
||||
// Gruppiere nach Datenpunkt
|
||||
const consentMap: Record<string, { granted: boolean; grantedAt: Date; revokedAt?: Date }> = {}
|
||||
for (const consent of userConsents) {
|
||||
consentMap[consent.dataPointId] = {
|
||||
granted: consent.granted && !consent.revokedAt,
|
||||
grantedAt: consent.grantedAt,
|
||||
revokedAt: consent.revokedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
userId,
|
||||
consents: consentMap,
|
||||
totalConsents: Object.keys(consentMap).length,
|
||||
activeConsents: Object.values(consentMap).filter((c) => c.granted).length,
|
||||
})
|
||||
}
|
||||
|
||||
// Statistiken fuer alle Consents
|
||||
if (includeStats) {
|
||||
const stats = calculateStatistics(tenantConsents)
|
||||
return NextResponse.json({
|
||||
statistics: stats,
|
||||
recentConsents: tenantConsents
|
||||
.sort((a, b) => new Date(b.grantedAt).getTime() - new Date(a.grantedAt).getTime())
|
||||
.slice(0, 10)
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
userId: c.userId.substring(0, 8) + '...', // Anonymisiert
|
||||
dataPointId: c.dataPointId,
|
||||
granted: c.granted,
|
||||
grantedAt: c.grantedAt,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
// Standard: Alle Consents (anonymisiert)
|
||||
return NextResponse.json({
|
||||
totalConsents: tenantConsents.length,
|
||||
activeConsents: tenantConsents.filter((c) => c.granted && !c.revokedAt).length,
|
||||
revokedConsents: tenantConsents.filter((c) => c.revokedAt).length,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching consents:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch consents' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/sdk/v1/einwilligungen/consent
|
||||
*
|
||||
* Batch-Update von Consents (z.B. Cookie-Banner)
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const tenantId = request.headers.get('X-Tenant-ID')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { userId, consents, consentVersion = '1.0.0' } = body
|
||||
|
||||
if (!userId || !consents || typeof consents !== 'object') {
|
||||
return NextResponse.json(
|
||||
{ error: 'userId and consents object required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null
|
||||
const userAgent = request.headers.get('user-agent') || null
|
||||
|
||||
const tenantConsents = consentStorage.get(tenantId) || []
|
||||
const now = new Date()
|
||||
|
||||
// Verarbeite jeden Consent
|
||||
for (const [dataPointId, granted] of Object.entries(consents)) {
|
||||
if (typeof granted !== 'boolean') continue
|
||||
|
||||
const existingIndex = tenantConsents.findIndex(
|
||||
(c) => c.userId === userId && c.dataPointId === dataPointId && !c.revokedAt
|
||||
)
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
const existing = tenantConsents[existingIndex]
|
||||
if (existing.granted !== granted) {
|
||||
if (!granted) {
|
||||
// Widerruf
|
||||
tenantConsents[existingIndex].revokedAt = now
|
||||
} else {
|
||||
// Neuer Consent nach Widerruf
|
||||
tenantConsents.push({
|
||||
id: generateId(),
|
||||
userId,
|
||||
dataPointId,
|
||||
granted: true,
|
||||
grantedAt: now,
|
||||
ipAddress: ipAddress || undefined,
|
||||
userAgent: userAgent || undefined,
|
||||
consentVersion,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (granted) {
|
||||
// Neuer Consent
|
||||
tenantConsents.push({
|
||||
id: generateId(),
|
||||
userId,
|
||||
dataPointId,
|
||||
granted: true,
|
||||
grantedAt: now,
|
||||
ipAddress: ipAddress || undefined,
|
||||
userAgent: userAgent || undefined,
|
||||
consentVersion,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
consentStorage.set(tenantId, tenantConsents)
|
||||
|
||||
// Zaehle aktive Consents fuer diesen User
|
||||
const activeConsents = tenantConsents.filter(
|
||||
(c) => c.userId === userId && c.granted && !c.revokedAt
|
||||
).length
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
userId,
|
||||
activeConsents,
|
||||
updatedAt: now,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error updating consents:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update consents' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet Consent-Statistiken
|
||||
*/
|
||||
function calculateStatistics(consents: ConsentEntry[]): ConsentStatistics {
|
||||
const activeConsents = consents.filter((c) => c.granted && !c.revokedAt)
|
||||
const revokedConsents = consents.filter((c) => c.revokedAt)
|
||||
|
||||
// Gruppiere nach Kategorie (18 Kategorien A-R)
|
||||
const byCategory: Record<DataPointCategory, { total: number; active: number; revoked: number }> = {
|
||||
MASTER_DATA: { total: 0, active: 0, revoked: 0 },
|
||||
CONTACT_DATA: { total: 0, active: 0, revoked: 0 },
|
||||
AUTHENTICATION: { total: 0, active: 0, revoked: 0 },
|
||||
CONSENT: { total: 0, active: 0, revoked: 0 },
|
||||
COMMUNICATION: { total: 0, active: 0, revoked: 0 },
|
||||
PAYMENT: { total: 0, active: 0, revoked: 0 },
|
||||
USAGE_DATA: { total: 0, active: 0, revoked: 0 },
|
||||
LOCATION: { total: 0, active: 0, revoked: 0 },
|
||||
DEVICE_DATA: { total: 0, active: 0, revoked: 0 },
|
||||
MARKETING: { total: 0, active: 0, revoked: 0 },
|
||||
ANALYTICS: { total: 0, active: 0, revoked: 0 },
|
||||
SOCIAL_MEDIA: { total: 0, active: 0, revoked: 0 },
|
||||
HEALTH_DATA: { total: 0, active: 0, revoked: 0 },
|
||||
EMPLOYEE_DATA: { total: 0, active: 0, revoked: 0 },
|
||||
CONTRACT_DATA: { total: 0, active: 0, revoked: 0 },
|
||||
LOG_DATA: { total: 0, active: 0, revoked: 0 },
|
||||
AI_DATA: { total: 0, active: 0, revoked: 0 },
|
||||
SECURITY: { total: 0, active: 0, revoked: 0 },
|
||||
}
|
||||
|
||||
for (const consent of consents) {
|
||||
const dataPoint = PREDEFINED_DATA_POINTS.find((dp) => dp.id === consent.dataPointId)
|
||||
if (dataPoint) {
|
||||
byCategory[dataPoint.category].total++
|
||||
if (consent.granted && !consent.revokedAt) {
|
||||
byCategory[dataPoint.category].active++
|
||||
}
|
||||
if (consent.revokedAt) {
|
||||
byCategory[dataPoint.category].revoked++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gruppiere nach Rechtsgrundlage (7 Rechtsgrundlagen)
|
||||
const byLegalBasis: Record<LegalBasis, { total: number; active: number }> = {
|
||||
CONTRACT: { total: 0, active: 0 },
|
||||
CONSENT: { total: 0, active: 0 },
|
||||
EXPLICIT_CONSENT: { total: 0, active: 0 },
|
||||
LEGITIMATE_INTEREST: { total: 0, active: 0 },
|
||||
LEGAL_OBLIGATION: { total: 0, active: 0 },
|
||||
VITAL_INTERESTS: { total: 0, active: 0 },
|
||||
PUBLIC_INTEREST: { total: 0, active: 0 },
|
||||
}
|
||||
|
||||
for (const consent of consents) {
|
||||
const dataPoint = PREDEFINED_DATA_POINTS.find((dp) => dp.id === consent.dataPointId)
|
||||
if (dataPoint) {
|
||||
byLegalBasis[dataPoint.legalBasis].total++
|
||||
if (consent.granted && !consent.revokedAt) {
|
||||
byLegalBasis[dataPoint.legalBasis].active++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Berechne Conversion Rate (Unique Users mit mindestens einem Consent)
|
||||
const uniqueUsers = new Set(consents.map((c) => c.userId))
|
||||
const usersWithActiveConsent = new Set(activeConsents.map((c) => c.userId))
|
||||
const conversionRate = uniqueUsers.size > 0
|
||||
? (usersWithActiveConsent.size / uniqueUsers.size) * 100
|
||||
: 0
|
||||
|
||||
return {
|
||||
totalConsents: consents.length,
|
||||
activeConsents: activeConsents.length,
|
||||
revokedConsents: revokedConsents.length,
|
||||
byCategory,
|
||||
byLegalBasis,
|
||||
conversionRate: Math.round(conversionRate * 10) / 10,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* API Route: Cookie Banner Configuration
|
||||
*
|
||||
* GET - Cookie Banner Konfiguration abrufen
|
||||
* POST - Cookie Banner Konfiguration speichern
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import {
|
||||
CookieBannerConfig,
|
||||
CookieBannerStyling,
|
||||
CookieBannerTexts,
|
||||
DataPoint,
|
||||
} from '@/lib/sdk/einwilligungen/types'
|
||||
import {
|
||||
generateCookieBannerConfig,
|
||||
DEFAULT_COOKIE_BANNER_STYLING,
|
||||
DEFAULT_COOKIE_BANNER_TEXTS,
|
||||
} from '@/lib/sdk/einwilligungen/generator/cookie-banner'
|
||||
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
|
||||
|
||||
// In-Memory Storage fuer Cookie Banner Configs
|
||||
const configStorage = new Map<string, CookieBannerConfig>()
|
||||
|
||||
/**
|
||||
* GET /api/sdk/v1/einwilligungen/cookie-banner/config
|
||||
*
|
||||
* Laedt die Cookie Banner Konfiguration fuer einen Tenant
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const tenantId = request.headers.get('X-Tenant-ID')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
let config = configStorage.get(tenantId)
|
||||
|
||||
if (!config) {
|
||||
// Generiere Default-Konfiguration
|
||||
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
|
||||
configStorage.set(tenantId, config)
|
||||
}
|
||||
|
||||
return NextResponse.json(config)
|
||||
} catch (error) {
|
||||
console.error('Error loading cookie banner config:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to load cookie banner config' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sdk/v1/einwilligungen/cookie-banner/config
|
||||
*
|
||||
* Speichert oder aktualisiert die Cookie Banner Konfiguration
|
||||
*
|
||||
* Body:
|
||||
* - dataPointIds?: string[] - IDs der Datenpunkte (fuer Neuberechnung)
|
||||
* - styling?: Partial<CookieBannerStyling> - Styling-Optionen
|
||||
* - texts?: Partial<CookieBannerTexts> - Text-Optionen
|
||||
* - customDataPoints?: DataPoint[] - Kundenspezifische Datenpunkte
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const tenantId = request.headers.get('X-Tenant-ID')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const {
|
||||
dataPointIds,
|
||||
styling,
|
||||
texts,
|
||||
customDataPoints = [],
|
||||
} = body
|
||||
|
||||
// Hole bestehende Konfiguration oder erstelle neue
|
||||
let config = configStorage.get(tenantId)
|
||||
|
||||
if (dataPointIds && Array.isArray(dataPointIds)) {
|
||||
// Neu berechnen basierend auf Datenpunkten
|
||||
const allDataPoints: DataPoint[] = [
|
||||
...PREDEFINED_DATA_POINTS,
|
||||
...customDataPoints,
|
||||
]
|
||||
|
||||
const selectedDataPoints = dataPointIds
|
||||
.map((id: string) => allDataPoints.find((dp) => dp.id === id))
|
||||
.filter((dp): dp is DataPoint => dp !== undefined)
|
||||
|
||||
config = generateCookieBannerConfig(
|
||||
tenantId,
|
||||
selectedDataPoints,
|
||||
texts,
|
||||
styling
|
||||
)
|
||||
} else if (config) {
|
||||
// Nur Styling/Texts aktualisieren
|
||||
if (styling) {
|
||||
config.styling = {
|
||||
...config.styling,
|
||||
...styling,
|
||||
}
|
||||
}
|
||||
if (texts) {
|
||||
config.texts = {
|
||||
...config.texts,
|
||||
...texts,
|
||||
}
|
||||
}
|
||||
config.updatedAt = new Date()
|
||||
} else {
|
||||
// Erstelle Default
|
||||
config = generateCookieBannerConfig(
|
||||
tenantId,
|
||||
PREDEFINED_DATA_POINTS,
|
||||
texts,
|
||||
styling
|
||||
)
|
||||
}
|
||||
|
||||
configStorage.set(tenantId, config)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
config,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error saving cookie banner config:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to save cookie banner config' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/sdk/v1/einwilligungen/cookie-banner/config
|
||||
*
|
||||
* Aktualisiert einzelne Kategorien
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const tenantId = request.headers.get('X-Tenant-ID')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { categoryId, enabled } = body
|
||||
|
||||
if (!categoryId || typeof enabled !== 'boolean') {
|
||||
return NextResponse.json(
|
||||
{ error: 'categoryId and enabled required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
let config = configStorage.get(tenantId)
|
||||
|
||||
if (!config) {
|
||||
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
|
||||
}
|
||||
|
||||
// Finde und aktualisiere die Kategorie
|
||||
const categoryIndex = config.categories.findIndex((c) => c.id === categoryId)
|
||||
|
||||
if (categoryIndex === -1) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Category not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Essenzielle Cookies koennen nicht deaktiviert werden
|
||||
if (config.categories[categoryIndex].isRequired && !enabled) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Essential cookies cannot be disabled' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
config.categories[categoryIndex].defaultEnabled = enabled
|
||||
config.updatedAt = new Date()
|
||||
|
||||
configStorage.set(tenantId, config)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
category: config.categories[categoryIndex],
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error updating cookie category:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update cookie category' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* API Route: Cookie Banner Embed Code
|
||||
*
|
||||
* GET - Generiert den Embed-Code fuer den Cookie Banner
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { CookieBannerConfig, CookieBannerEmbedCode } from '@/lib/sdk/einwilligungen/types'
|
||||
import {
|
||||
generateCookieBannerConfig,
|
||||
generateEmbedCode,
|
||||
} from '@/lib/sdk/einwilligungen/generator/cookie-banner'
|
||||
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
|
||||
|
||||
// In-Memory Storage (in Produktion mit configStorage aus config/route.ts teilen)
|
||||
const configStorage = new Map<string, CookieBannerConfig>()
|
||||
|
||||
/**
|
||||
* GET /api/sdk/v1/einwilligungen/cookie-banner/embed-code
|
||||
*
|
||||
* Generiert den Embed-Code fuer den Cookie Banner
|
||||
*
|
||||
* Query Parameters:
|
||||
* - privacyPolicyUrl: string - URL zur Datenschutzerklaerung (default: /datenschutz)
|
||||
* - format: 'combined' | 'separate' - Ausgabeformat (default: combined)
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const tenantId = request.headers.get('X-Tenant-ID')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const privacyPolicyUrl = searchParams.get('privacyPolicyUrl') || '/datenschutz'
|
||||
const format = searchParams.get('format') || 'combined'
|
||||
|
||||
// Hole oder erstelle Konfiguration
|
||||
let config = configStorage.get(tenantId)
|
||||
|
||||
if (!config) {
|
||||
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
|
||||
configStorage.set(tenantId, config)
|
||||
}
|
||||
|
||||
// Generiere Embed-Code
|
||||
const embedCode = generateEmbedCode(config, privacyPolicyUrl)
|
||||
|
||||
if (format === 'separate') {
|
||||
// Separate Dateien zurueckgeben
|
||||
return NextResponse.json({
|
||||
html: embedCode.html,
|
||||
css: embedCode.css,
|
||||
js: embedCode.js,
|
||||
scriptTag: embedCode.scriptTag,
|
||||
instructions: {
|
||||
de: `
|
||||
Fuegen Sie den folgenden Code in Ihre Website ein:
|
||||
|
||||
1. CSS in den <head>-Bereich:
|
||||
<style>${embedCode.css}</style>
|
||||
|
||||
2. HTML vor dem schliessenden </body>-Tag:
|
||||
${embedCode.html}
|
||||
|
||||
3. JavaScript vor dem schliessenden </body>-Tag:
|
||||
<script>${embedCode.js}</script>
|
||||
|
||||
Alternativ koennen Sie die Dateien separat einbinden:
|
||||
- /cookie-banner.css
|
||||
- /cookie-banner.js
|
||||
`,
|
||||
en: `
|
||||
Add the following code to your website:
|
||||
|
||||
1. CSS in the <head> section:
|
||||
<style>${embedCode.css}</style>
|
||||
|
||||
2. HTML before the closing </body> tag:
|
||||
${embedCode.html}
|
||||
|
||||
3. JavaScript before the closing </body> tag:
|
||||
<script>${embedCode.js}</script>
|
||||
|
||||
Alternatively, you can include the files separately:
|
||||
- /cookie-banner.css
|
||||
- /cookie-banner.js
|
||||
`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Combined: Alles in einem HTML-Block
|
||||
const combinedCode = `
|
||||
<!-- Cookie Banner - Start -->
|
||||
<style>
|
||||
${embedCode.css}
|
||||
</style>
|
||||
|
||||
${embedCode.html}
|
||||
|
||||
<script>
|
||||
${embedCode.js}
|
||||
</script>
|
||||
<!-- Cookie Banner - End -->
|
||||
`.trim()
|
||||
|
||||
return NextResponse.json({
|
||||
embedCode: combinedCode,
|
||||
scriptTag: embedCode.scriptTag,
|
||||
config: {
|
||||
tenantId: config.tenantId,
|
||||
categories: config.categories.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
isRequired: c.isRequired,
|
||||
defaultEnabled: c.defaultEnabled,
|
||||
})),
|
||||
styling: config.styling,
|
||||
},
|
||||
instructions: {
|
||||
de: `Fuegen Sie den folgenden Code vor dem schliessenden </body>-Tag Ihrer Website ein.`,
|
||||
en: `Add the following code before the closing </body> tag of your website.`,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error generating embed code:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate embed code' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sdk/v1/einwilligungen/cookie-banner/embed-code
|
||||
*
|
||||
* Generiert Embed-Code mit benutzerdefinierten Optionen
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const tenantId = request.headers.get('X-Tenant-ID')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const {
|
||||
privacyPolicyUrl = '/datenschutz',
|
||||
styling,
|
||||
texts,
|
||||
language = 'de',
|
||||
} = body
|
||||
|
||||
// Hole oder erstelle Konfiguration
|
||||
let config = configStorage.get(tenantId)
|
||||
|
||||
if (!config) {
|
||||
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS, texts, styling)
|
||||
} else {
|
||||
// Wende temporaere Anpassungen an
|
||||
if (styling) {
|
||||
config = {
|
||||
...config,
|
||||
styling: { ...config.styling, ...styling },
|
||||
}
|
||||
}
|
||||
if (texts) {
|
||||
config = {
|
||||
...config,
|
||||
texts: { ...config.texts, ...texts },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const embedCode = generateEmbedCode(config, privacyPolicyUrl)
|
||||
|
||||
// Generiere Preview HTML
|
||||
const previewHtml = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="${language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cookie Banner Preview</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f1f5f9;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.preview-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 { color: #1e293b; }
|
||||
p { color: #64748b; line-height: 1.6; }
|
||||
${embedCode.css}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="preview-content">
|
||||
<h1>Cookie Banner Preview</h1>
|
||||
<p>Dies ist eine Vorschau des Cookie Banners. In der produktiven Umgebung wird der Banner auf Ihrer Website angezeigt.</p>
|
||||
</div>
|
||||
|
||||
${embedCode.html}
|
||||
|
||||
<script>
|
||||
${embedCode.js}
|
||||
// Force show banner for preview
|
||||
setTimeout(() => {
|
||||
document.getElementById('cookieBanner')?.classList.add('active');
|
||||
document.getElementById('cookieBannerOverlay')?.classList.add('active');
|
||||
}, 100);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`.trim()
|
||||
|
||||
return NextResponse.json({
|
||||
embedCode: {
|
||||
html: embedCode.html,
|
||||
css: embedCode.css,
|
||||
js: embedCode.js,
|
||||
scriptTag: embedCode.scriptTag,
|
||||
},
|
||||
previewHtml,
|
||||
config: {
|
||||
tenantId: config.tenantId,
|
||||
categories: config.categories.length,
|
||||
styling: config.styling,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error generating custom embed code:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate embed code' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* API Route: Privacy Policy Generator
|
||||
*
|
||||
* POST - Generiert eine Datenschutzerklaerung aus dem Datenpunktkatalog
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import {
|
||||
CompanyInfo,
|
||||
DataPoint,
|
||||
SupportedLanguage,
|
||||
ExportFormat,
|
||||
GeneratedPrivacyPolicy,
|
||||
} from '@/lib/sdk/einwilligungen/types'
|
||||
import {
|
||||
generatePrivacyPolicy,
|
||||
generatePrivacyPolicySections,
|
||||
} from '@/lib/sdk/einwilligungen/generator/privacy-policy'
|
||||
import {
|
||||
PREDEFINED_DATA_POINTS,
|
||||
getDataPointById,
|
||||
} from '@/lib/sdk/einwilligungen/catalog/loader'
|
||||
|
||||
// In-Memory Storage fuer generierte Policies
|
||||
const policyStorage = new Map<string, GeneratedPrivacyPolicy>()
|
||||
|
||||
/**
|
||||
* POST /api/sdk/v1/einwilligungen/privacy-policy/generate
|
||||
*
|
||||
* Generiert eine Datenschutzerklaerung
|
||||
*
|
||||
* Body:
|
||||
* - dataPointIds: string[] - IDs der zu inkludierenden Datenpunkte
|
||||
* - companyInfo: CompanyInfo - Firmeninformationen
|
||||
* - language: 'de' | 'en' - Sprache
|
||||
* - format: 'HTML' | 'MARKDOWN' | 'PDF' | 'DOCX' - Ausgabeformat
|
||||
* - customDataPoints?: DataPoint[] - Kundenspezifische Datenpunkte
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const tenantId = request.headers.get('X-Tenant-ID')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tenant ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const {
|
||||
dataPointIds,
|
||||
companyInfo,
|
||||
language = 'de',
|
||||
format = 'HTML',
|
||||
customDataPoints = [],
|
||||
} = body
|
||||
|
||||
// Validierung
|
||||
if (!companyInfo || !companyInfo.name || !companyInfo.address || !companyInfo.email) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Company info (name, address, email) required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!dataPointIds || !Array.isArray(dataPointIds) || dataPointIds.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'At least one data point ID required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validiere Sprache
|
||||
const validLanguages: SupportedLanguage[] = ['de', 'en']
|
||||
if (!validLanguages.includes(language)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid language. Must be "de" or "en"' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validiere Format
|
||||
const validFormats: ExportFormat[] = ['HTML', 'MARKDOWN', 'PDF', 'DOCX']
|
||||
if (!validFormats.includes(format)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid format. Must be HTML, MARKDOWN, PDF, or DOCX' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Sammle alle Datenpunkte
|
||||
const allDataPoints: DataPoint[] = [
|
||||
...PREDEFINED_DATA_POINTS,
|
||||
...customDataPoints,
|
||||
]
|
||||
|
||||
// Filtere nach ausgewaehlten IDs
|
||||
const selectedDataPoints = dataPointIds
|
||||
.map((id: string) => allDataPoints.find((dp) => dp.id === id))
|
||||
.filter((dp): dp is DataPoint => dp !== undefined)
|
||||
|
||||
if (selectedDataPoints.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No valid data points found for the provided IDs' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generiere die Privacy Policy
|
||||
const policy = generatePrivacyPolicy(
|
||||
tenantId,
|
||||
selectedDataPoints,
|
||||
companyInfo as CompanyInfo,
|
||||
language as SupportedLanguage,
|
||||
format as ExportFormat
|
||||
)
|
||||
|
||||
// Speichere fuer spaeteres Abrufen
|
||||
policyStorage.set(policy.id, policy)
|
||||
|
||||
// Fuer PDF/DOCX: Nur Metadaten zurueckgeben, Download separat
|
||||
if (format === 'PDF' || format === 'DOCX') {
|
||||
return NextResponse.json({
|
||||
id: policy.id,
|
||||
tenantId: policy.tenantId,
|
||||
language: policy.language,
|
||||
format: policy.format,
|
||||
generatedAt: policy.generatedAt,
|
||||
version: policy.version,
|
||||
sections: policy.sections.map((s) => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
order: s.order,
|
||||
})),
|
||||
downloadUrl: `/api/sdk/v1/einwilligungen/privacy-policy/${policy.id}/download`,
|
||||
})
|
||||
}
|
||||
|
||||
// Fuer HTML/Markdown: Vollstaendige Policy zurueckgeben
|
||||
return NextResponse.json(policy)
|
||||
} catch (error) {
|
||||
console.error('Error generating privacy policy:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate privacy policy' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sdk/v1/einwilligungen/privacy-policy/generate
|
||||
*
|
||||
* Liefert eine Vorschau der Abschnitte ohne vollstaendige Generierung
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const language = (searchParams.get('language') as SupportedLanguage) || 'de'
|
||||
|
||||
// Liefere die Standard-Abschnittsstruktur
|
||||
const sections = [
|
||||
{ id: 'controller', order: 1, title: { de: '1. Verantwortlicher', en: '1. Data Controller' } },
|
||||
{ id: 'data-collection', order: 2, title: { de: '2. Erhobene personenbezogene Daten', en: '2. Personal Data We Collect' } },
|
||||
{ id: 'purposes', order: 3, title: { de: '3. Zwecke der Datenverarbeitung', en: '3. Purposes of Data Processing' } },
|
||||
{ id: 'legal-basis', order: 4, title: { de: '4. Rechtsgrundlagen der Verarbeitung', en: '4. Legal Basis for Processing' } },
|
||||
{ id: 'recipients', order: 5, title: { de: '5. Empfaenger und Datenweitergabe', en: '5. Recipients and Data Sharing' } },
|
||||
{ id: 'retention', order: 6, title: { de: '6. Speicherdauer', en: '6. Data Retention' } },
|
||||
{ id: 'rights', order: 7, title: { de: '7. Ihre Rechte als betroffene Person', en: '7. Your Rights as a Data Subject' } },
|
||||
{ id: 'cookies', order: 8, title: { de: '8. Cookies und aehnliche Technologien', en: '8. Cookies and Similar Technologies' } },
|
||||
{ id: 'changes', order: 9, title: { de: '9. Aenderungen dieser Datenschutzerklaerung', en: '9. Changes to this Privacy Policy' } },
|
||||
]
|
||||
|
||||
return NextResponse.json({
|
||||
sections,
|
||||
availableLanguages: ['de', 'en'],
|
||||
availableFormats: ['HTML', 'MARKDOWN', 'PDF', 'DOCX'],
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching sections:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch sections' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
88
admin-v2/app/api/sdk/v1/export/route.ts
Normal file
88
admin-v2/app/api/sdk/v1/export/route.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* SDK Export API
|
||||
*
|
||||
* GET /api/sdk/v1/export?format=json|pdf|zip - Export SDK data
|
||||
*/
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const format = searchParams.get('format') || 'json'
|
||||
const tenantId = searchParams.get('tenantId') || 'default'
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return exportJSON(tenantId)
|
||||
|
||||
case 'pdf':
|
||||
return exportPDF(tenantId)
|
||||
|
||||
case 'zip':
|
||||
return exportZIP(tenantId)
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: `Unknown export format: ${format}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to export:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to export' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function exportJSON(tenantId: string) {
|
||||
// In production, this would fetch the actual state from the database
|
||||
const exportData = {
|
||||
version: '1.0.0',
|
||||
exportedAt: new Date().toISOString(),
|
||||
tenantId,
|
||||
data: {
|
||||
useCases: [],
|
||||
screening: null,
|
||||
modules: [],
|
||||
requirements: [],
|
||||
controls: [],
|
||||
evidence: [],
|
||||
risks: [],
|
||||
dsfa: null,
|
||||
toms: [],
|
||||
vvt: [],
|
||||
documents: [],
|
||||
},
|
||||
}
|
||||
|
||||
return new NextResponse(JSON.stringify(exportData, null, 2), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Disposition': `attachment; filename="compliance-export-${tenantId}-${new Date().toISOString().split('T')[0]}.json"`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function exportPDF(tenantId: string) {
|
||||
// In production, this would generate a proper PDF using a library like pdfkit or puppeteer
|
||||
// For now, return a placeholder response
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'PDF export not yet implemented',
|
||||
message: 'PDF generation requires server-side rendering. Use JSON export for now.',
|
||||
}, { status: 501 })
|
||||
}
|
||||
|
||||
function exportZIP(tenantId: string) {
|
||||
// In production, this would create a ZIP file with multiple documents
|
||||
// For now, return a placeholder response
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'ZIP export not yet implemented',
|
||||
message: 'ZIP generation requires additional server-side processing. Use JSON export for now.',
|
||||
}, { status: 501 })
|
||||
}
|
||||
150
admin-v2/app/api/sdk/v1/flow/route.ts
Normal file
150
admin-v2/app/api/sdk/v1/flow/route.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* SDK Flow API
|
||||
*
|
||||
* GET /api/sdk/v1/flow - Get current flow state and suggestions
|
||||
* POST /api/sdk/v1/flow/next - Navigate to next step
|
||||
* POST /api/sdk/v1/flow/previous - Navigate to previous step
|
||||
*/
|
||||
|
||||
const SDK_STEPS = [
|
||||
// Phase 1
|
||||
{ id: 'company-profile', phase: 1, order: 1, name: 'Unternehmensprofil', url: '/sdk/company-profile' },
|
||||
{ id: 'use-case-assessment', phase: 1, order: 2, name: 'Anwendungsfall-Erfassung', url: '/sdk/advisory-board' },
|
||||
{ id: 'screening', phase: 1, order: 3, name: 'System Screening', url: '/sdk/screening' },
|
||||
{ id: 'modules', phase: 1, order: 4, name: 'Compliance Modules', url: '/sdk/modules' },
|
||||
{ id: 'requirements', phase: 1, order: 5, name: 'Requirements', url: '/sdk/requirements' },
|
||||
{ id: 'controls', phase: 1, order: 6, name: 'Controls', url: '/sdk/controls' },
|
||||
{ id: 'evidence', phase: 1, order: 7, name: 'Evidence', url: '/sdk/evidence' },
|
||||
{ id: 'audit-checklist', phase: 1, order: 8, name: 'Audit Checklist', url: '/sdk/audit-checklist' },
|
||||
{ id: 'risks', phase: 1, order: 9, name: 'Risk Matrix', url: '/sdk/risks' },
|
||||
// Phase 2
|
||||
{ id: 'ai-act', phase: 2, order: 1, name: 'AI Act Klassifizierung', url: '/sdk/ai-act' },
|
||||
{ id: 'obligations', phase: 2, order: 2, name: 'Pflichtenübersicht', url: '/sdk/obligations' },
|
||||
{ id: 'dsfa', phase: 2, order: 3, name: 'DSFA', url: '/sdk/dsfa' },
|
||||
{ id: 'tom', phase: 2, order: 4, name: 'TOMs', url: '/sdk/tom' },
|
||||
{ id: 'loeschfristen', phase: 2, order: 5, name: 'Löschfristen', url: '/sdk/loeschfristen' },
|
||||
{ id: 'vvt', phase: 2, order: 6, name: 'Verarbeitungsverzeichnis', url: '/sdk/vvt' },
|
||||
{ id: 'consent', phase: 2, order: 7, name: 'Rechtliche Vorlagen', url: '/sdk/consent' },
|
||||
{ id: 'cookie-banner', phase: 2, order: 8, name: 'Cookie Banner', url: '/sdk/cookie-banner' },
|
||||
{ id: 'einwilligungen', phase: 2, order: 9, name: 'Einwilligungen', url: '/sdk/einwilligungen' },
|
||||
{ id: 'dsr', phase: 2, order: 10, name: 'DSR Portal', url: '/sdk/dsr' },
|
||||
{ id: 'escalations', phase: 2, order: 11, name: 'Escalations', url: '/sdk/escalations' },
|
||||
]
|
||||
|
||||
function getStepIndex(stepId: string): number {
|
||||
return SDK_STEPS.findIndex(s => s.id === stepId)
|
||||
}
|
||||
|
||||
function getNextStep(currentStepId: string) {
|
||||
const currentIndex = getStepIndex(currentStepId)
|
||||
if (currentIndex === -1 || currentIndex >= SDK_STEPS.length - 1) {
|
||||
return null
|
||||
}
|
||||
return SDK_STEPS[currentIndex + 1]
|
||||
}
|
||||
|
||||
function getPreviousStep(currentStepId: string) {
|
||||
const currentIndex = getStepIndex(currentStepId)
|
||||
if (currentIndex <= 0) {
|
||||
return null
|
||||
}
|
||||
return SDK_STEPS[currentIndex - 1]
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const currentStepId = searchParams.get('currentStep') || 'company-profile'
|
||||
|
||||
const currentStep = SDK_STEPS.find(s => s.id === currentStepId)
|
||||
const nextStep = getNextStep(currentStepId)
|
||||
const previousStep = getPreviousStep(currentStepId)
|
||||
|
||||
// Generate suggestions based on context
|
||||
const suggestions = [
|
||||
{
|
||||
type: 'NAVIGATION',
|
||||
label: nextStep ? `Weiter zu ${nextStep.name}` : 'Flow abgeschlossen',
|
||||
action: nextStep ? `navigate:${nextStep.url}` : null,
|
||||
},
|
||||
{
|
||||
type: 'ACTION',
|
||||
label: 'Checkpoint validieren',
|
||||
action: 'validate:current',
|
||||
},
|
||||
{
|
||||
type: 'HELP',
|
||||
label: 'Hilfe anzeigen',
|
||||
action: 'help:show',
|
||||
},
|
||||
]
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
currentStep,
|
||||
nextStep,
|
||||
previousStep,
|
||||
totalSteps: SDK_STEPS.length,
|
||||
currentIndex: getStepIndex(currentStepId) + 1,
|
||||
suggestions,
|
||||
steps: SDK_STEPS,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to get flow:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get flow' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { action, currentStepId } = body
|
||||
|
||||
if (!action || !currentStepId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'action and currentStepId are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
let targetStep = null
|
||||
|
||||
switch (action) {
|
||||
case 'next':
|
||||
targetStep = getNextStep(currentStepId)
|
||||
break
|
||||
case 'previous':
|
||||
targetStep = getPreviousStep(currentStepId)
|
||||
break
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid action' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!targetStep) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No target step available' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
targetStep,
|
||||
redirectUrl: targetStep.url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to navigate flow:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to navigate flow' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
309
admin-v2/app/api/sdk/v1/generate/route.ts
Normal file
309
admin-v2/app/api/sdk/v1/generate/route.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* SDK Document Generation API
|
||||
*
|
||||
* POST /api/sdk/v1/generate - Generate compliance documents
|
||||
*
|
||||
* Supported document types:
|
||||
* - dsfa: Data Protection Impact Assessment
|
||||
* - tom: Technical and Organizational Measures
|
||||
* - vvt: Processing Register (Art. 30 GDPR)
|
||||
* - cookie-banner: Cookie consent banner code
|
||||
* - audit-report: Audit report
|
||||
*/
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { documentType, context, options } = body
|
||||
|
||||
if (!documentType) {
|
||||
return NextResponse.json(
|
||||
{ error: 'documentType is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate document based on type
|
||||
let document: unknown = null
|
||||
let generationTime = Date.now()
|
||||
|
||||
switch (documentType) {
|
||||
case 'dsfa':
|
||||
document = generateDSFA(context, options)
|
||||
break
|
||||
|
||||
case 'tom':
|
||||
document = generateTOMs(context, options)
|
||||
break
|
||||
|
||||
case 'vvt':
|
||||
document = generateVVT(context, options)
|
||||
break
|
||||
|
||||
case 'cookie-banner':
|
||||
document = generateCookieBanner(context, options)
|
||||
break
|
||||
|
||||
case 'audit-report':
|
||||
document = generateAuditReport(context, options)
|
||||
break
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: `Unknown document type: ${documentType}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
generationTime = Date.now() - generationTime
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
documentType,
|
||||
document,
|
||||
generatedAt: new Date().toISOString(),
|
||||
generationTimeMs: generationTime,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to generate document:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate document' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DOCUMENT GENERATORS
|
||||
// =============================================================================
|
||||
|
||||
function generateDSFA(context: unknown, options: unknown) {
|
||||
return {
|
||||
id: `dsfa-${Date.now()}`,
|
||||
status: 'DRAFT',
|
||||
version: 1,
|
||||
sections: [
|
||||
{
|
||||
id: 'section-1',
|
||||
title: '1. Systematische Beschreibung der Verarbeitungsvorgänge',
|
||||
content: 'Die geplante Verarbeitung umfasst...',
|
||||
status: 'DRAFT',
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
id: 'section-2',
|
||||
title: '2. Bewertung der Notwendigkeit und Verhältnismäßigkeit',
|
||||
content: 'Die Verarbeitung ist notwendig für...',
|
||||
status: 'DRAFT',
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
id: 'section-3',
|
||||
title: '3. Bewertung der Risiken für die Rechte und Freiheiten',
|
||||
content: 'Identifizierte Risiken:\n- Risiko 1\n- Risiko 2',
|
||||
status: 'DRAFT',
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
id: 'section-4',
|
||||
title: '4. Abhilfemaßnahmen',
|
||||
content: 'Folgende Maßnahmen werden ergriffen...',
|
||||
status: 'DRAFT',
|
||||
order: 4,
|
||||
},
|
||||
],
|
||||
approvals: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
function generateTOMs(context: unknown, options: unknown) {
|
||||
return {
|
||||
toms: [
|
||||
{
|
||||
id: 'tom-1',
|
||||
category: 'Zutrittskontrolle',
|
||||
name: 'Physische Zugangskontrollen',
|
||||
description: 'Maßnahmen zur Verhinderung unbefugten Zutritts zu Datenverarbeitungsanlagen',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'NOT_IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
},
|
||||
{
|
||||
id: 'tom-2',
|
||||
category: 'Zugangskontrolle',
|
||||
name: 'Authentifizierung',
|
||||
description: 'Multi-Faktor-Authentifizierung für alle Systeme',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'NOT_IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
},
|
||||
{
|
||||
id: 'tom-3',
|
||||
category: 'Zugriffskontrolle',
|
||||
name: 'Rollenbasierte Zugriffskontrolle',
|
||||
description: 'RBAC-System für granulare Berechtigungsvergabe',
|
||||
type: 'ORGANIZATIONAL',
|
||||
implementationStatus: 'NOT_IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
},
|
||||
{
|
||||
id: 'tom-4',
|
||||
category: 'Weitergabekontrolle',
|
||||
name: 'Verschlüsselung',
|
||||
description: 'Ende-zu-Ende-Verschlüsselung für Datenübertragung',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'NOT_IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
},
|
||||
{
|
||||
id: 'tom-5',
|
||||
category: 'Eingabekontrolle',
|
||||
name: 'Audit Logging',
|
||||
description: 'Protokollierung aller Dateneingaben und -änderungen',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'NOT_IMPLEMENTED',
|
||||
priority: 'MEDIUM',
|
||||
},
|
||||
],
|
||||
generatedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
function generateVVT(context: unknown, options: unknown) {
|
||||
return {
|
||||
processingActivities: [
|
||||
{
|
||||
id: 'pa-1',
|
||||
name: 'Kundenmanagement',
|
||||
purpose: 'Verwaltung von Kundenbeziehungen und Aufträgen',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (Vertrag)',
|
||||
dataCategories: ['Name', 'Kontaktdaten', 'Bestellhistorie'],
|
||||
dataSubjects: ['Kunden'],
|
||||
recipients: ['Interne Mitarbeiter', 'Zahlungsdienstleister'],
|
||||
thirdCountryTransfers: false,
|
||||
retentionPeriod: '10 Jahre (handelsrechtliche Aufbewahrungspflicht)',
|
||||
technicalMeasures: ['Verschlüsselung', 'Zugriffskontrolle'],
|
||||
organizationalMeasures: ['Schulungen', 'Vertraulichkeitsverpflichtung'],
|
||||
},
|
||||
],
|
||||
generatedAt: new Date().toISOString(),
|
||||
version: '1.0',
|
||||
}
|
||||
}
|
||||
|
||||
function generateCookieBanner(context: unknown, options: unknown) {
|
||||
return {
|
||||
id: `cookie-${Date.now()}`,
|
||||
style: 'BANNER',
|
||||
position: 'BOTTOM',
|
||||
theme: 'LIGHT',
|
||||
texts: {
|
||||
title: 'Cookie-Einstellungen',
|
||||
description: 'Wir verwenden Cookies, um Ihnen die beste Nutzererfahrung zu bieten.',
|
||||
acceptAll: 'Alle akzeptieren',
|
||||
rejectAll: 'Alle ablehnen',
|
||||
settings: 'Einstellungen',
|
||||
save: 'Speichern',
|
||||
},
|
||||
categories: [
|
||||
{
|
||||
id: 'necessary',
|
||||
name: 'Notwendig',
|
||||
description: 'Diese Cookies sind für die Grundfunktionen erforderlich.',
|
||||
required: true,
|
||||
cookies: [],
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
name: 'Analyse',
|
||||
description: 'Diese Cookies helfen uns, die Nutzung zu verstehen.',
|
||||
required: false,
|
||||
cookies: [],
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
name: 'Marketing',
|
||||
description: 'Diese Cookies werden für Werbezwecke verwendet.',
|
||||
required: false,
|
||||
cookies: [],
|
||||
},
|
||||
],
|
||||
generatedCode: {
|
||||
html: `<!-- Cookie Banner HTML -->
|
||||
<div id="cookie-banner" class="cookie-banner">
|
||||
<div class="cookie-content">
|
||||
<h3>Cookie-Einstellungen</h3>
|
||||
<p>Wir verwenden Cookies, um Ihnen die beste Nutzererfahrung zu bieten.</p>
|
||||
<div class="cookie-actions">
|
||||
<button onclick="acceptAll()">Alle akzeptieren</button>
|
||||
<button onclick="rejectAll()">Alle ablehnen</button>
|
||||
<button onclick="showSettings()">Einstellungen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
css: `.cookie-banner {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
|
||||
padding: 20px;
|
||||
z-index: 9999;
|
||||
}
|
||||
.cookie-content { max-width: 1200px; margin: 0 auto; }
|
||||
.cookie-actions { margin-top: 15px; display: flex; gap: 10px; }
|
||||
.cookie-actions button { padding: 10px 20px; border-radius: 5px; cursor: pointer; }`,
|
||||
js: `function acceptAll() {
|
||||
setCookie('consent', 'all', 365);
|
||||
document.getElementById('cookie-banner').style.display = 'none';
|
||||
}
|
||||
function rejectAll() {
|
||||
setCookie('consent', 'necessary', 365);
|
||||
document.getElementById('cookie-banner').style.display = 'none';
|
||||
}
|
||||
function setCookie(name, value, days) {
|
||||
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||||
document.cookie = name + '=' + value + '; expires=' + expires + '; path=/; SameSite=Lax';
|
||||
}`,
|
||||
},
|
||||
generatedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
function generateAuditReport(context: unknown, options: unknown) {
|
||||
return {
|
||||
id: `audit-${Date.now()}`,
|
||||
title: 'Compliance Audit Report',
|
||||
generatedAt: new Date().toISOString(),
|
||||
summary: {
|
||||
totalChecks: 50,
|
||||
passed: 35,
|
||||
failed: 10,
|
||||
warnings: 5,
|
||||
complianceScore: 70,
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: 'Executive Summary',
|
||||
content: 'Dieser Bericht fasst den aktuellen Compliance-Status zusammen...',
|
||||
},
|
||||
{
|
||||
title: 'Methodik',
|
||||
content: 'Die Prüfung wurde gemäß ISO 27001 und DSGVO durchgeführt...',
|
||||
},
|
||||
{
|
||||
title: 'Ergebnisse',
|
||||
content: 'Hauptabweichungen: 3\nNebenabweichungen: 7\nEmpfehlungen: 5',
|
||||
},
|
||||
{
|
||||
title: 'Empfehlungen',
|
||||
content: '1. Implementierung von MFA\n2. Verbesserung der Dokumentation\n3. Regelmäßige Schulungen',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
345
admin-v2/app/api/sdk/v1/state/route.ts
Normal file
345
admin-v2/app/api/sdk/v1/state/route.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* SDK State Management API
|
||||
*
|
||||
* GET /api/sdk/v1/state?tenantId=xxx - Load state for a tenant
|
||||
* POST /api/sdk/v1/state - Save state for a tenant
|
||||
* DELETE /api/sdk/v1/state?tenantId=xxx - Clear state for a tenant
|
||||
*
|
||||
* Features:
|
||||
* - Versioning for optimistic locking
|
||||
* - Last-Modified headers
|
||||
* - ETag support for caching
|
||||
* - Prepared for PostgreSQL migration
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
interface StoredState {
|
||||
state: unknown
|
||||
version: number
|
||||
userId?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STORAGE LAYER (Abstract - Easy to swap to PostgreSQL)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* In-memory storage for development
|
||||
* TODO: Replace with PostgreSQL implementation
|
||||
*
|
||||
* PostgreSQL Schema:
|
||||
* CREATE TABLE sdk_states (
|
||||
* id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
* tenant_id VARCHAR(255) NOT NULL UNIQUE,
|
||||
* user_id VARCHAR(255),
|
||||
* state JSONB NOT NULL,
|
||||
* version INTEGER DEFAULT 1,
|
||||
* created_at TIMESTAMP DEFAULT NOW(),
|
||||
* updated_at TIMESTAMP DEFAULT NOW()
|
||||
* );
|
||||
*
|
||||
* CREATE INDEX idx_sdk_states_tenant ON sdk_states(tenant_id);
|
||||
*/
|
||||
|
||||
interface StateStore {
|
||||
get(tenantId: string): Promise<StoredState | null>
|
||||
save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState>
|
||||
delete(tenantId: string): Promise<boolean>
|
||||
}
|
||||
|
||||
class InMemoryStateStore implements StateStore {
|
||||
private store: Map<string, StoredState> = new Map()
|
||||
|
||||
async get(tenantId: string): Promise<StoredState | null> {
|
||||
return this.store.get(tenantId) || null
|
||||
}
|
||||
|
||||
async save(
|
||||
tenantId: string,
|
||||
state: unknown,
|
||||
userId?: string,
|
||||
expectedVersion?: number
|
||||
): Promise<StoredState> {
|
||||
const existing = this.store.get(tenantId)
|
||||
|
||||
// Optimistic locking check
|
||||
if (expectedVersion !== undefined && existing && existing.version !== expectedVersion) {
|
||||
const error = new Error('Version conflict') as Error & { status: number }
|
||||
error.status = 409
|
||||
throw error
|
||||
}
|
||||
|
||||
const now = new Date().toISOString()
|
||||
const newVersion = existing ? existing.version + 1 : 1
|
||||
|
||||
const stored: StoredState = {
|
||||
state: {
|
||||
...(state as object),
|
||||
lastModified: now,
|
||||
},
|
||||
version: newVersion,
|
||||
userId,
|
||||
createdAt: existing?.createdAt || now,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
this.store.set(tenantId, stored)
|
||||
return stored
|
||||
}
|
||||
|
||||
async delete(tenantId: string): Promise<boolean> {
|
||||
return this.store.delete(tenantId)
|
||||
}
|
||||
}
|
||||
|
||||
// Future PostgreSQL implementation would look like:
|
||||
// class PostgreSQLStateStore implements StateStore {
|
||||
// private db: Pool
|
||||
//
|
||||
// constructor(connectionString: string) {
|
||||
// this.db = new Pool({ connectionString })
|
||||
// }
|
||||
//
|
||||
// async get(tenantId: string): Promise<StoredState | null> {
|
||||
// const result = await this.db.query(
|
||||
// 'SELECT state, version, user_id, created_at, updated_at FROM sdk_states WHERE tenant_id = $1',
|
||||
// [tenantId]
|
||||
// )
|
||||
// if (result.rows.length === 0) return null
|
||||
// const row = result.rows[0]
|
||||
// return {
|
||||
// state: row.state,
|
||||
// version: row.version,
|
||||
// userId: row.user_id,
|
||||
// createdAt: row.created_at,
|
||||
// updatedAt: row.updated_at,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// async save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState> {
|
||||
// // Use UPSERT with version check
|
||||
// const result = await this.db.query(`
|
||||
// INSERT INTO sdk_states (tenant_id, user_id, state, version)
|
||||
// VALUES ($1, $2, $3, 1)
|
||||
// ON CONFLICT (tenant_id) DO UPDATE SET
|
||||
// state = $3,
|
||||
// user_id = COALESCE($2, sdk_states.user_id),
|
||||
// version = sdk_states.version + 1,
|
||||
// updated_at = NOW()
|
||||
// WHERE ($4::int IS NULL OR sdk_states.version = $4)
|
||||
// RETURNING version, created_at, updated_at
|
||||
// `, [tenantId, userId, JSON.stringify(state), expectedVersion])
|
||||
//
|
||||
// if (result.rows.length === 0) {
|
||||
// throw new Error('Version conflict')
|
||||
// }
|
||||
//
|
||||
// return {
|
||||
// state,
|
||||
// version: result.rows[0].version,
|
||||
// userId,
|
||||
// createdAt: result.rows[0].created_at,
|
||||
// updatedAt: result.rows[0].updated_at,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// async delete(tenantId: string): Promise<boolean> {
|
||||
// const result = await this.db.query(
|
||||
// 'DELETE FROM sdk_states WHERE tenant_id = $1',
|
||||
// [tenantId]
|
||||
// )
|
||||
// return result.rowCount > 0
|
||||
// }
|
||||
// }
|
||||
|
||||
// Use in-memory store for now
|
||||
const stateStore: StateStore = new InMemoryStateStore()
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function generateETag(version: number, updatedAt: string): string {
|
||||
return `"${version}-${Buffer.from(updatedAt).toString('base64').slice(0, 8)}"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HANDLERS
|
||||
// =============================================================================
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const tenantId = searchParams.get('tenantId')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'tenantId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const stored = await stateStore.get(tenantId)
|
||||
|
||||
if (!stored) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'State not found', tenantId },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const etag = generateETag(stored.version, stored.updatedAt)
|
||||
|
||||
// Check If-None-Match header for caching
|
||||
const ifNoneMatch = request.headers.get('If-None-Match')
|
||||
if (ifNoneMatch === etag) {
|
||||
return new NextResponse(null, { status: 304 })
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: {
|
||||
tenantId,
|
||||
state: stored.state,
|
||||
version: stored.version,
|
||||
lastModified: stored.updatedAt,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'ETag': etag,
|
||||
'Last-Modified': stored.updatedAt,
|
||||
'Cache-Control': 'private, no-cache',
|
||||
},
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to load SDK state:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to load state' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { tenantId, state, version } = body
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'tenantId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'state is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check If-Match header for optimistic locking
|
||||
const ifMatch = request.headers.get('If-Match')
|
||||
const expectedVersion = ifMatch ? parseInt(ifMatch, 10) : version
|
||||
|
||||
const stored = await stateStore.save(tenantId, state, body.userId, expectedVersion)
|
||||
|
||||
const etag = generateETag(stored.version, stored.updatedAt)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: {
|
||||
tenantId,
|
||||
state: stored.state,
|
||||
version: stored.version,
|
||||
lastModified: stored.updatedAt,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'ETag': etag,
|
||||
'Last-Modified': stored.updatedAt,
|
||||
},
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
const err = error as Error & { status?: number }
|
||||
|
||||
// Handle version conflict
|
||||
if (err.status === 409 || err.message === 'Version conflict') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Version conflict. State was modified by another request.',
|
||||
code: 'VERSION_CONFLICT',
|
||||
},
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
console.error('Failed to save SDK state:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to save state' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const tenantId = searchParams.get('tenantId')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'tenantId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const deleted = await stateStore.delete(tenantId)
|
||||
|
||||
if (!deleted) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'State not found', tenantId },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
tenantId,
|
||||
deletedAt: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to delete SDK state:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to delete state' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HEALTH CHECK
|
||||
// =============================================================================
|
||||
|
||||
export async function OPTIONS() {
|
||||
return NextResponse.json({ status: 'ok' }, {
|
||||
headers: {
|
||||
'Allow': 'GET, POST, DELETE, OPTIONS',
|
||||
},
|
||||
})
|
||||
}
|
||||
107
admin-v2/app/api/sdk/v1/tom-generator/controls/evaluate/route.ts
Normal file
107
admin-v2/app/api/sdk/v1/tom-generator/controls/evaluate/route.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { TOMRulesEngine } from '@/lib/sdk/tom-generator/rules-engine'
|
||||
import { TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
|
||||
|
||||
/**
|
||||
* TOM Generator Controls Evaluation API
|
||||
*
|
||||
* POST /api/sdk/v1/tom-generator/controls/evaluate - Evaluate controls for given state
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* state: TOMGeneratorState
|
||||
* }
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* evaluations: RulesEngineResult[]
|
||||
* derivedTOMs: DerivedTOM[]
|
||||
* summary: {
|
||||
* total: number
|
||||
* required: number
|
||||
* recommended: number
|
||||
* optional: number
|
||||
* notApplicable: number
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { state } = body
|
||||
|
||||
if (!state) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'state is required in request body' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Parse dates in state
|
||||
const parsedState: TOMGeneratorState = {
|
||||
...state,
|
||||
createdAt: new Date(state.createdAt),
|
||||
updatedAt: new Date(state.updatedAt),
|
||||
steps: state.steps?.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
|
||||
...step,
|
||||
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
|
||||
})) || [],
|
||||
documents: [],
|
||||
derivedTOMs: [],
|
||||
gapAnalysis: null,
|
||||
exports: [],
|
||||
}
|
||||
|
||||
// Initialize rules engine and evaluate
|
||||
const engine = new TOMRulesEngine()
|
||||
const evaluations = engine.evaluateControls(parsedState)
|
||||
const derivedTOMs = engine.deriveAllTOMs(parsedState)
|
||||
|
||||
// Calculate summary
|
||||
const summary = {
|
||||
total: evaluations.length,
|
||||
required: evaluations.filter((e) => e.applicability === 'REQUIRED').length,
|
||||
recommended: evaluations.filter((e) => e.applicability === 'RECOMMENDED').length,
|
||||
optional: evaluations.filter((e) => e.applicability === 'OPTIONAL').length,
|
||||
notApplicable: evaluations.filter((e) => e.applicability === 'NOT_APPLICABLE').length,
|
||||
}
|
||||
|
||||
// Group by category
|
||||
const byCategory: Record<string, typeof evaluations> = {}
|
||||
evaluations.forEach((e) => {
|
||||
const category = e.controlId.split('-')[1] // Extract category from ID like TOM-AC-01
|
||||
if (!byCategory[category]) {
|
||||
byCategory[category] = []
|
||||
}
|
||||
byCategory[category].push(e)
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
evaluations,
|
||||
derivedTOMs,
|
||||
summary,
|
||||
byCategory,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to evaluate controls:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to evaluate controls' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return NextResponse.json(
|
||||
{ status: 'ok' },
|
||||
{
|
||||
headers: {
|
||||
Allow: 'POST, OPTIONS',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
128
admin-v2/app/api/sdk/v1/tom-generator/controls/route.ts
Normal file
128
admin-v2/app/api/sdk/v1/tom-generator/controls/route.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import {
|
||||
getAllControls,
|
||||
getControlById,
|
||||
getControlsByCategory,
|
||||
searchControls,
|
||||
getCategories,
|
||||
} from '@/lib/sdk/tom-generator/controls/loader'
|
||||
import { ControlCategory } from '@/lib/sdk/tom-generator/types'
|
||||
|
||||
/**
|
||||
* TOM Generator Controls API
|
||||
*
|
||||
* GET /api/sdk/v1/tom-generator/controls - List all controls
|
||||
* GET /api/sdk/v1/tom-generator/controls?id=xxx - Get single control
|
||||
* GET /api/sdk/v1/tom-generator/controls?category=xxx - Filter by category
|
||||
* GET /api/sdk/v1/tom-generator/controls?search=xxx - Search controls
|
||||
* GET /api/sdk/v1/tom-generator/controls?categories=true - Get categories list
|
||||
*/
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const id = searchParams.get('id')
|
||||
const category = searchParams.get('category')
|
||||
const search = searchParams.get('search')
|
||||
const categoriesOnly = searchParams.get('categories')
|
||||
const language = (searchParams.get('language') || 'de') as 'de' | 'en'
|
||||
|
||||
// Get categories list
|
||||
if (categoriesOnly === 'true') {
|
||||
const categories = getCategories()
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: categories,
|
||||
})
|
||||
}
|
||||
|
||||
// Get single control by ID
|
||||
if (id) {
|
||||
const control = getControlById(id)
|
||||
if (!control) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Control not found: ${id}` },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
...control,
|
||||
// Return localized name and description
|
||||
localizedName: control.name[language],
|
||||
localizedDescription: control.description[language],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Filter by category
|
||||
if (category) {
|
||||
const controls = getControlsByCategory(category as ControlCategory)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: controls.map((c) => ({
|
||||
...c,
|
||||
localizedName: c.name[language],
|
||||
localizedDescription: c.description[language],
|
||||
})),
|
||||
meta: {
|
||||
category,
|
||||
count: controls.length,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Search controls
|
||||
if (search) {
|
||||
const controls = searchControls(search, language)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: controls.map((c) => ({
|
||||
...c,
|
||||
localizedName: c.name[language],
|
||||
localizedDescription: c.description[language],
|
||||
})),
|
||||
meta: {
|
||||
query: search,
|
||||
count: controls.length,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Return all controls
|
||||
const controls = getAllControls()
|
||||
const categories = getCategories()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: controls.map((c) => ({
|
||||
...c,
|
||||
localizedName: c.name[language],
|
||||
localizedDescription: c.description[language],
|
||||
})),
|
||||
meta: {
|
||||
totalControls: controls.length,
|
||||
categories: categories.length,
|
||||
language,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch controls:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch controls' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return NextResponse.json(
|
||||
{ status: 'ok' },
|
||||
{
|
||||
headers: {
|
||||
Allow: 'GET, OPTIONS',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { TOMDocumentAnalyzer } from '@/lib/sdk/tom-generator/ai/document-analyzer'
|
||||
import { evidenceStore } from '@/lib/sdk/tom-generator/evidence-store'
|
||||
|
||||
/**
|
||||
* TOM Generator Evidence Analysis API
|
||||
*
|
||||
* POST /api/sdk/v1/tom-generator/evidence/[id]/analyze - Analyze evidence document with AI
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* tenantId: string
|
||||
* documentText?: string (if already extracted)
|
||||
* }
|
||||
*/
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const { tenantId, documentText } = body
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'tenantId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get the document
|
||||
const document = await evidenceStore.getById(tenantId, id)
|
||||
if (!document) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Document not found: ${id}` },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if already analyzed
|
||||
if (document.aiAnalysis && document.status === 'ANALYZED') {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: document.aiAnalysis,
|
||||
meta: {
|
||||
alreadyAnalyzed: true,
|
||||
analyzedAt: document.aiAnalysis.analyzedAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Get document text (in production, this would be extracted from the file)
|
||||
const text = documentText || `[Document content from ${document.originalName}]`
|
||||
|
||||
// Initialize analyzer
|
||||
const analyzer = new TOMDocumentAnalyzer()
|
||||
|
||||
// Analyze the document
|
||||
const analysisResult = await analyzer.analyzeDocument(
|
||||
document,
|
||||
text,
|
||||
'de'
|
||||
)
|
||||
|
||||
// Check if analysis was successful
|
||||
if (!analysisResult.success || !analysisResult.analysis) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: analysisResult.error || 'Analysis failed' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
const analysis = analysisResult.analysis
|
||||
|
||||
// Update the document with analysis results
|
||||
const updatedDocument = await evidenceStore.update(tenantId, id, {
|
||||
aiAnalysis: analysis,
|
||||
status: 'ANALYZED',
|
||||
linkedControlIds: [
|
||||
...new Set([
|
||||
...document.linkedControlIds,
|
||||
...analysis.applicableControls,
|
||||
]),
|
||||
],
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
analysis,
|
||||
document: updatedDocument,
|
||||
},
|
||||
meta: {
|
||||
documentId: id,
|
||||
analyzedAt: analysis.analyzedAt,
|
||||
confidence: analysis.confidence,
|
||||
applicableControlsCount: analysis.applicableControls.length,
|
||||
gapsCount: analysis.gaps.length,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to analyze evidence:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to analyze evidence' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return NextResponse.json(
|
||||
{ status: 'ok' },
|
||||
{
|
||||
headers: {
|
||||
Allow: 'POST, OPTIONS',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
153
admin-v2/app/api/sdk/v1/tom-generator/evidence/route.ts
Normal file
153
admin-v2/app/api/sdk/v1/tom-generator/evidence/route.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { DocumentType } from '@/lib/sdk/tom-generator/types'
|
||||
import { evidenceStore } from '@/lib/sdk/tom-generator/evidence-store'
|
||||
|
||||
/**
|
||||
* TOM Generator Evidence API
|
||||
*
|
||||
* GET /api/sdk/v1/tom-generator/evidence?tenantId=xxx - List all evidence documents
|
||||
* DELETE /api/sdk/v1/tom-generator/evidence?tenantId=xxx&id=xxx - Delete evidence
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// HANDLERS
|
||||
// =============================================================================
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const tenantId = searchParams.get('tenantId')
|
||||
const documentType = searchParams.get('type') as DocumentType | null
|
||||
const status = searchParams.get('status')
|
||||
const id = searchParams.get('id')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'tenantId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get single document
|
||||
if (id) {
|
||||
const document = await evidenceStore.getById(tenantId, id)
|
||||
if (!document) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Document not found: ${id}` },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: document,
|
||||
})
|
||||
}
|
||||
|
||||
// Filter by type
|
||||
if (documentType) {
|
||||
const documents = await evidenceStore.getByType(tenantId, documentType)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: documents,
|
||||
meta: {
|
||||
count: documents.length,
|
||||
filter: { type: documentType },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if (status) {
|
||||
const documents = await evidenceStore.getByStatus(tenantId, status)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: documents,
|
||||
meta: {
|
||||
count: documents.length,
|
||||
filter: { status },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Get all documents
|
||||
const documents = await evidenceStore.getAll(tenantId)
|
||||
|
||||
// Group by type for summary
|
||||
const byType: Record<string, number> = {}
|
||||
const byStatus: Record<string, number> = {}
|
||||
documents.forEach((doc) => {
|
||||
byType[doc.documentType] = (byType[doc.documentType] || 0) + 1
|
||||
byStatus[doc.status] = (byStatus[doc.status] || 0) + 1
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: documents,
|
||||
meta: {
|
||||
count: documents.length,
|
||||
byType,
|
||||
byStatus,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch evidence:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch evidence' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const tenantId = searchParams.get('tenantId')
|
||||
const id = searchParams.get('id')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'tenantId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'id is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const deleted = await evidenceStore.delete(tenantId, id)
|
||||
|
||||
if (!deleted) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Document not found: ${id}` },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
id,
|
||||
deletedAt: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to delete evidence:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to delete evidence' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return NextResponse.json(
|
||||
{ status: 'ok' },
|
||||
{
|
||||
headers: {
|
||||
Allow: 'GET, DELETE, OPTIONS',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
155
admin-v2/app/api/sdk/v1/tom-generator/evidence/upload/route.ts
Normal file
155
admin-v2/app/api/sdk/v1/tom-generator/evidence/upload/route.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { EvidenceDocument, DocumentType } from '@/lib/sdk/tom-generator/types'
|
||||
import { evidenceStore } from '@/lib/sdk/tom-generator/evidence-store'
|
||||
import crypto from 'crypto'
|
||||
|
||||
/**
|
||||
* TOM Generator Evidence Upload API
|
||||
*
|
||||
* POST /api/sdk/v1/tom-generator/evidence/upload - Upload evidence document
|
||||
*
|
||||
* Request: multipart/form-data
|
||||
* - file: File
|
||||
* - tenantId: string
|
||||
* - documentType: DocumentType
|
||||
* - validFrom?: string (ISO date)
|
||||
* - validUntil?: string (ISO date)
|
||||
* - linkedControlIds?: string (comma-separated)
|
||||
*/
|
||||
|
||||
// Document type detection based on filename patterns
|
||||
function detectDocumentType(filename: string, mimeType: string): DocumentType {
|
||||
const lower = filename.toLowerCase()
|
||||
|
||||
if (lower.includes('avv') || lower.includes('auftragsverarbeitung')) {
|
||||
return 'AVV'
|
||||
}
|
||||
if (lower.includes('dpa') || lower.includes('data processing')) {
|
||||
return 'DPA'
|
||||
}
|
||||
if (lower.includes('sla') || lower.includes('service level')) {
|
||||
return 'SLA'
|
||||
}
|
||||
if (lower.includes('nda') || lower.includes('vertraulichkeit') || lower.includes('geheimhaltung')) {
|
||||
return 'NDA'
|
||||
}
|
||||
if (lower.includes('policy') || lower.includes('richtlinie')) {
|
||||
return 'POLICY'
|
||||
}
|
||||
if (lower.includes('cert') || lower.includes('zertifikat') || lower.includes('iso')) {
|
||||
return 'CERTIFICATE'
|
||||
}
|
||||
if (lower.includes('audit') || lower.includes('prüf') || lower.includes('bericht')) {
|
||||
return 'AUDIT_REPORT'
|
||||
}
|
||||
|
||||
return 'OTHER'
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const file = formData.get('file') as File | null
|
||||
const tenantId = formData.get('tenantId') as string | null
|
||||
const documentType = formData.get('documentType') as DocumentType | null
|
||||
const validFrom = formData.get('validFrom') as string | null
|
||||
const validUntil = formData.get('validUntil') as string | null
|
||||
const linkedControlIdsStr = formData.get('linkedControlIds') as string | null
|
||||
const uploadedBy = formData.get('uploadedBy') as string | null
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'file is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'tenantId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Read file data
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(arrayBuffer)
|
||||
|
||||
// Generate hash for deduplication
|
||||
const hash = crypto.createHash('sha256').update(buffer).digest('hex')
|
||||
|
||||
// Generate unique filename
|
||||
const id = crypto.randomUUID()
|
||||
const ext = file.name.split('.').pop() || 'bin'
|
||||
const filename = `${id}.${ext}`
|
||||
|
||||
// Detect document type if not provided
|
||||
const detectedType = detectDocumentType(file.name, file.type)
|
||||
const finalDocumentType = documentType || detectedType
|
||||
|
||||
// Parse linked control IDs
|
||||
const linkedControlIds = linkedControlIdsStr
|
||||
? linkedControlIdsStr.split(',').map((s) => s.trim()).filter(Boolean)
|
||||
: []
|
||||
|
||||
// Create evidence document
|
||||
const document: EvidenceDocument = {
|
||||
id,
|
||||
filename,
|
||||
originalName: file.name,
|
||||
mimeType: file.type,
|
||||
size: file.size,
|
||||
uploadedAt: new Date(),
|
||||
uploadedBy: uploadedBy || 'unknown',
|
||||
documentType: finalDocumentType,
|
||||
detectedType,
|
||||
hash,
|
||||
validFrom: validFrom ? new Date(validFrom) : null,
|
||||
validUntil: validUntil ? new Date(validUntil) : null,
|
||||
linkedControlIds,
|
||||
aiAnalysis: null,
|
||||
status: 'PENDING',
|
||||
}
|
||||
|
||||
// Store the document metadata
|
||||
// Note: In production, the actual file would be stored in MinIO/S3
|
||||
await evidenceStore.add(tenantId, document)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: document.id,
|
||||
filename: document.filename,
|
||||
originalName: document.originalName,
|
||||
mimeType: document.mimeType,
|
||||
size: document.size,
|
||||
documentType: document.documentType,
|
||||
detectedType: document.detectedType,
|
||||
status: document.status,
|
||||
uploadedAt: document.uploadedAt.toISOString(),
|
||||
},
|
||||
meta: {
|
||||
hash,
|
||||
needsAnalysis: true,
|
||||
analyzeUrl: `/api/sdk/v1/tom-generator/evidence/${id}/analyze`,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to upload evidence:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to upload evidence' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return NextResponse.json(
|
||||
{ status: 'ok' },
|
||||
{
|
||||
headers: {
|
||||
Allow: 'POST, OPTIONS',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
245
admin-v2/app/api/sdk/v1/tom-generator/export/route.ts
Normal file
245
admin-v2/app/api/sdk/v1/tom-generator/export/route.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
|
||||
import { generateDOCXContent, generateDOCXFilename } from '@/lib/sdk/tom-generator/export/docx'
|
||||
import { generatePDFContent, generatePDFFilename } from '@/lib/sdk/tom-generator/export/pdf'
|
||||
import { generateZIPFiles, generateZIPFilename } from '@/lib/sdk/tom-generator/export/zip'
|
||||
import crypto from 'crypto'
|
||||
|
||||
/**
|
||||
* TOM Generator Export API
|
||||
*
|
||||
* POST /api/sdk/v1/tom-generator/export - Generate export
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* tenantId: string
|
||||
* format: 'DOCX' | 'PDF' | 'JSON' | 'ZIP'
|
||||
* language: 'de' | 'en'
|
||||
* state: TOMGeneratorState
|
||||
* options?: {
|
||||
* includeEvidence?: boolean
|
||||
* includeGapAnalysis?: boolean
|
||||
* companyLogo?: string (base64)
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
// In-memory export store for tracking exports
|
||||
interface StoredExport {
|
||||
id: string
|
||||
tenantId: string
|
||||
format: string
|
||||
filename: string
|
||||
content: string // Base64 encoded content
|
||||
generatedAt: Date
|
||||
size: number
|
||||
}
|
||||
|
||||
const exportStore: Map<string, StoredExport> = new Map()
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { tenantId, format, language = 'de', state, options = {} } = body
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'tenantId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!format) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'format is required (DOCX, PDF, JSON, ZIP)' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'state is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Parse dates in state
|
||||
const parsedState: TOMGeneratorState = {
|
||||
...state,
|
||||
createdAt: new Date(state.createdAt),
|
||||
updatedAt: new Date(state.updatedAt),
|
||||
steps: state.steps?.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
|
||||
...step,
|
||||
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
|
||||
})) || [],
|
||||
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
|
||||
...doc,
|
||||
uploadedAt: new Date(doc.uploadedAt),
|
||||
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
|
||||
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
|
||||
aiAnalysis: doc.aiAnalysis ? {
|
||||
...doc.aiAnalysis,
|
||||
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
|
||||
} : null,
|
||||
})) || [],
|
||||
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
|
||||
...tom,
|
||||
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
|
||||
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
|
||||
})) || [],
|
||||
gapAnalysis: state.gapAnalysis ? {
|
||||
...state.gapAnalysis,
|
||||
generatedAt: new Date(state.gapAnalysis.generatedAt),
|
||||
} : null,
|
||||
exports: state.exports?.map((exp: { generatedAt: string }) => ({
|
||||
...exp,
|
||||
generatedAt: new Date(exp.generatedAt),
|
||||
})) || [],
|
||||
}
|
||||
|
||||
const exportId = crypto.randomUUID()
|
||||
let content: string
|
||||
let filename: string
|
||||
let mimeType: string
|
||||
|
||||
switch (format.toUpperCase()) {
|
||||
case 'DOCX': {
|
||||
// Generate DOCX structure (actual binary conversion would require docx library)
|
||||
const docxContent = generateDOCXContent(parsedState, { language: language as 'de' | 'en', ...options })
|
||||
content = Buffer.from(JSON.stringify(docxContent, null, 2)).toString('base64')
|
||||
filename = generateDOCXFilename(parsedState, language as 'de' | 'en')
|
||||
mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
break
|
||||
}
|
||||
|
||||
case 'PDF': {
|
||||
// Generate PDF structure (actual binary conversion would require pdf library)
|
||||
const pdfContent = generatePDFContent(parsedState, { language: language as 'de' | 'en', ...options })
|
||||
content = Buffer.from(JSON.stringify(pdfContent, null, 2)).toString('base64')
|
||||
filename = generatePDFFilename(parsedState, language as 'de' | 'en')
|
||||
mimeType = 'application/pdf'
|
||||
break
|
||||
}
|
||||
|
||||
case 'JSON':
|
||||
content = Buffer.from(JSON.stringify(parsedState, null, 2)).toString('base64')
|
||||
filename = `tom-export-${tenantId}-${new Date().toISOString().split('T')[0]}.json`
|
||||
mimeType = 'application/json'
|
||||
break
|
||||
|
||||
case 'ZIP': {
|
||||
const files = generateZIPFiles(parsedState, { language: language as 'de' | 'en', ...options })
|
||||
// For now, return the files metadata (actual ZIP generation would require a library)
|
||||
content = Buffer.from(JSON.stringify(files, null, 2)).toString('base64')
|
||||
filename = generateZIPFilename(parsedState, language as 'de' | 'en')
|
||||
mimeType = 'application/zip'
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Unsupported format: ${format}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Store the export
|
||||
const storedExport: StoredExport = {
|
||||
id: exportId,
|
||||
tenantId,
|
||||
format: format.toUpperCase(),
|
||||
filename,
|
||||
content,
|
||||
generatedAt: new Date(),
|
||||
size: Buffer.from(content, 'base64').length,
|
||||
}
|
||||
exportStore.set(exportId, storedExport)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
exportId,
|
||||
filename,
|
||||
format: format.toUpperCase(),
|
||||
mimeType,
|
||||
size: storedExport.size,
|
||||
generatedAt: storedExport.generatedAt.toISOString(),
|
||||
downloadUrl: `/api/sdk/v1/tom-generator/export?exportId=${exportId}`,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to generate export:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to generate export' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const exportId = searchParams.get('exportId')
|
||||
|
||||
if (!exportId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'exportId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const storedExport = exportStore.get(exportId)
|
||||
if (!storedExport) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Export not found: ${exportId}` },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Return the file as download
|
||||
const buffer = Buffer.from(storedExport.content, 'base64')
|
||||
|
||||
let mimeType: string
|
||||
switch (storedExport.format) {
|
||||
case 'DOCX':
|
||||
mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
break
|
||||
case 'PDF':
|
||||
mimeType = 'application/pdf'
|
||||
break
|
||||
case 'JSON':
|
||||
mimeType = 'application/json'
|
||||
break
|
||||
case 'ZIP':
|
||||
mimeType = 'application/zip'
|
||||
break
|
||||
default:
|
||||
mimeType = 'application/octet-stream'
|
||||
}
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': mimeType,
|
||||
'Content-Disposition': `attachment; filename="${storedExport.filename}"`,
|
||||
'Content-Length': buffer.length.toString(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to download export:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to download export' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return NextResponse.json(
|
||||
{ status: 'ok' },
|
||||
{
|
||||
headers: {
|
||||
Allow: 'GET, POST, OPTIONS',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
205
admin-v2/app/api/sdk/v1/tom-generator/gap-analysis/route.ts
Normal file
205
admin-v2/app/api/sdk/v1/tom-generator/gap-analysis/route.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { TOMRulesEngine } from '@/lib/sdk/tom-generator/rules-engine'
|
||||
import { TOMGeneratorState, GapAnalysisResult } from '@/lib/sdk/tom-generator/types'
|
||||
|
||||
/**
|
||||
* TOM Generator Gap Analysis API
|
||||
*
|
||||
* POST /api/sdk/v1/tom-generator/gap-analysis - Perform gap analysis
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* tenantId: string
|
||||
* state: TOMGeneratorState
|
||||
* }
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* gapAnalysis: GapAnalysisResult
|
||||
* }
|
||||
*/
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { tenantId, state } = body
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'tenantId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'state is required in request body' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Parse dates in state
|
||||
const parsedState: TOMGeneratorState = {
|
||||
...state,
|
||||
createdAt: new Date(state.createdAt),
|
||||
updatedAt: new Date(state.updatedAt),
|
||||
steps: state.steps?.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
|
||||
...step,
|
||||
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
|
||||
})) || [],
|
||||
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
|
||||
...doc,
|
||||
uploadedAt: new Date(doc.uploadedAt),
|
||||
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
|
||||
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
|
||||
aiAnalysis: doc.aiAnalysis ? {
|
||||
...doc.aiAnalysis,
|
||||
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
|
||||
} : null,
|
||||
})) || [],
|
||||
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
|
||||
...tom,
|
||||
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
|
||||
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
|
||||
})) || [],
|
||||
gapAnalysis: state.gapAnalysis ? {
|
||||
...state.gapAnalysis,
|
||||
generatedAt: new Date(state.gapAnalysis.generatedAt),
|
||||
} : null,
|
||||
exports: state.exports?.map((exp: { generatedAt: string }) => ({
|
||||
...exp,
|
||||
generatedAt: new Date(exp.generatedAt),
|
||||
})) || [],
|
||||
}
|
||||
|
||||
// Initialize rules engine
|
||||
const engine = new TOMRulesEngine()
|
||||
|
||||
// Perform gap analysis using derived TOMs and documents from state
|
||||
const gapAnalysis = engine.performGapAnalysis(
|
||||
parsedState.derivedTOMs,
|
||||
parsedState.documents
|
||||
)
|
||||
|
||||
// Calculate detailed metrics
|
||||
const metrics = calculateGapMetrics(gapAnalysis)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
gapAnalysis,
|
||||
metrics,
|
||||
generatedAt: gapAnalysis.generatedAt.toISOString(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to perform gap analysis:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to perform gap analysis' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function calculateGapMetrics(gapAnalysis: GapAnalysisResult) {
|
||||
const totalGaps = gapAnalysis.missingControls.length +
|
||||
gapAnalysis.partialControls.length +
|
||||
gapAnalysis.missingEvidence.length
|
||||
|
||||
const criticalGaps = gapAnalysis.missingControls.filter(
|
||||
(c) => c.priority === 'CRITICAL' || c.priority === 'HIGH'
|
||||
).length
|
||||
|
||||
const mediumGaps = gapAnalysis.missingControls.filter(
|
||||
(c) => c.priority === 'MEDIUM'
|
||||
).length
|
||||
|
||||
const lowGaps = gapAnalysis.missingControls.filter(
|
||||
(c) => c.priority === 'LOW'
|
||||
).length
|
||||
|
||||
// Group missing controls by category
|
||||
const gapsByCategory: Record<string, number> = {}
|
||||
gapAnalysis.missingControls.forEach((control) => {
|
||||
const category = control.controlId.split('-')[1] || 'OTHER'
|
||||
gapsByCategory[category] = (gapsByCategory[category] || 0) + 1
|
||||
})
|
||||
|
||||
// Calculate compliance readiness
|
||||
const maxScore = 100
|
||||
const deductionPerCritical = 10
|
||||
const deductionPerMedium = 5
|
||||
const deductionPerLow = 2
|
||||
const deductionPerPartial = 3
|
||||
const deductionPerMissingEvidence = 1
|
||||
|
||||
const deductions =
|
||||
criticalGaps * deductionPerCritical +
|
||||
mediumGaps * deductionPerMedium +
|
||||
lowGaps * deductionPerLow +
|
||||
gapAnalysis.partialControls.length * deductionPerPartial +
|
||||
gapAnalysis.missingEvidence.length * deductionPerMissingEvidence
|
||||
|
||||
const complianceReadiness = Math.max(0, Math.min(100, maxScore - deductions))
|
||||
|
||||
// Prioritized action items
|
||||
const prioritizedActions = [
|
||||
...gapAnalysis.missingControls
|
||||
.filter((c) => c.priority === 'CRITICAL')
|
||||
.map((c) => ({
|
||||
type: 'MISSING_CONTROL',
|
||||
priority: 'CRITICAL',
|
||||
controlId: c.controlId,
|
||||
reason: c.reason,
|
||||
action: `Implement control ${c.controlId}`,
|
||||
})),
|
||||
...gapAnalysis.missingControls
|
||||
.filter((c) => c.priority === 'HIGH')
|
||||
.map((c) => ({
|
||||
type: 'MISSING_CONTROL',
|
||||
priority: 'HIGH',
|
||||
controlId: c.controlId,
|
||||
reason: c.reason,
|
||||
action: `Implement control ${c.controlId}`,
|
||||
})),
|
||||
...gapAnalysis.partialControls.map((c) => ({
|
||||
type: 'PARTIAL_CONTROL',
|
||||
priority: 'MEDIUM',
|
||||
controlId: c.controlId,
|
||||
missingAspects: c.missingAspects,
|
||||
action: `Complete implementation of ${c.controlId}`,
|
||||
})),
|
||||
...gapAnalysis.missingEvidence.map((e) => ({
|
||||
type: 'MISSING_EVIDENCE',
|
||||
priority: 'LOW',
|
||||
controlId: e.controlId,
|
||||
requiredEvidence: e.requiredEvidence,
|
||||
action: `Upload evidence for ${e.controlId}`,
|
||||
})),
|
||||
]
|
||||
|
||||
return {
|
||||
totalGaps,
|
||||
criticalGaps,
|
||||
mediumGaps,
|
||||
lowGaps,
|
||||
partialControls: gapAnalysis.partialControls.length,
|
||||
missingEvidence: gapAnalysis.missingEvidence.length,
|
||||
gapsByCategory,
|
||||
complianceReadiness,
|
||||
overallScore: gapAnalysis.overallScore,
|
||||
prioritizedActionsCount: prioritizedActions.length,
|
||||
prioritizedActions: prioritizedActions.slice(0, 10), // Top 10 actions
|
||||
}
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return NextResponse.json(
|
||||
{ status: 'ok' },
|
||||
{
|
||||
headers: {
|
||||
Allow: 'POST, OPTIONS',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
250
admin-v2/app/api/sdk/v1/tom-generator/state/route.ts
Normal file
250
admin-v2/app/api/sdk/v1/tom-generator/state/route.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import {
|
||||
TOMGeneratorState,
|
||||
createEmptyTOMGeneratorState,
|
||||
} from '@/lib/sdk/tom-generator/types'
|
||||
|
||||
/**
|
||||
* TOM Generator State API
|
||||
*
|
||||
* GET /api/sdk/v1/tom-generator/state?tenantId=xxx - Load TOM generator state
|
||||
* POST /api/sdk/v1/tom-generator/state - Save TOM generator state
|
||||
* DELETE /api/sdk/v1/tom-generator/state?tenantId=xxx - Clear state
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// STORAGE (In-Memory for development)
|
||||
// =============================================================================
|
||||
|
||||
interface StoredTOMState {
|
||||
state: TOMGeneratorState
|
||||
version: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
class InMemoryTOMStateStore {
|
||||
private store: Map<string, StoredTOMState> = new Map()
|
||||
|
||||
async get(tenantId: string): Promise<StoredTOMState | null> {
|
||||
return this.store.get(tenantId) || null
|
||||
}
|
||||
|
||||
async save(tenantId: string, state: TOMGeneratorState, expectedVersion?: number): Promise<StoredTOMState> {
|
||||
const existing = this.store.get(tenantId)
|
||||
|
||||
if (expectedVersion !== undefined && existing && existing.version !== expectedVersion) {
|
||||
const error = new Error('Version conflict') as Error & { status: number }
|
||||
error.status = 409
|
||||
throw error
|
||||
}
|
||||
|
||||
const now = new Date().toISOString()
|
||||
const newVersion = existing ? existing.version + 1 : 1
|
||||
|
||||
const stored: StoredTOMState = {
|
||||
state: {
|
||||
...state,
|
||||
updatedAt: new Date(now),
|
||||
},
|
||||
version: newVersion,
|
||||
createdAt: existing?.createdAt || now,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
this.store.set(tenantId, stored)
|
||||
return stored
|
||||
}
|
||||
|
||||
async delete(tenantId: string): Promise<boolean> {
|
||||
return this.store.delete(tenantId)
|
||||
}
|
||||
|
||||
async list(): Promise<{ tenantId: string; updatedAt: string }[]> {
|
||||
const result: { tenantId: string; updatedAt: string }[] = []
|
||||
this.store.forEach((value, key) => {
|
||||
result.push({ tenantId: key, updatedAt: value.updatedAt })
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
const stateStore = new InMemoryTOMStateStore()
|
||||
|
||||
// =============================================================================
|
||||
// HANDLERS
|
||||
// =============================================================================
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const tenantId = searchParams.get('tenantId')
|
||||
|
||||
// List all states if no tenantId provided
|
||||
if (!tenantId) {
|
||||
const states = await stateStore.list()
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: states,
|
||||
})
|
||||
}
|
||||
|
||||
const stored = await stateStore.get(tenantId)
|
||||
|
||||
if (!stored) {
|
||||
// Return empty state for new tenants
|
||||
const emptyState = createEmptyTOMGeneratorState(tenantId)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
tenantId,
|
||||
state: emptyState,
|
||||
version: 0,
|
||||
isNew: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
tenantId,
|
||||
state: stored.state,
|
||||
version: stored.version,
|
||||
lastModified: stored.updatedAt,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to load TOM generator state:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to load state' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { tenantId, state, version } = body
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'tenantId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'state is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Deserialize dates
|
||||
const parsedState: TOMGeneratorState = {
|
||||
...state,
|
||||
createdAt: new Date(state.createdAt),
|
||||
updatedAt: new Date(state.updatedAt),
|
||||
steps: state.steps.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
|
||||
...step,
|
||||
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
|
||||
})),
|
||||
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
|
||||
...doc,
|
||||
uploadedAt: new Date(doc.uploadedAt),
|
||||
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
|
||||
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
|
||||
aiAnalysis: doc.aiAnalysis ? {
|
||||
...doc.aiAnalysis,
|
||||
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
|
||||
} : null,
|
||||
})) || [],
|
||||
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
|
||||
...tom,
|
||||
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
|
||||
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
|
||||
})) || [],
|
||||
gapAnalysis: state.gapAnalysis ? {
|
||||
...state.gapAnalysis,
|
||||
generatedAt: new Date(state.gapAnalysis.generatedAt),
|
||||
} : null,
|
||||
exports: state.exports?.map((exp: { generatedAt: string }) => ({
|
||||
...exp,
|
||||
generatedAt: new Date(exp.generatedAt),
|
||||
})) || [],
|
||||
}
|
||||
|
||||
const stored = await stateStore.save(tenantId, parsedState, version)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
tenantId,
|
||||
state: stored.state,
|
||||
version: stored.version,
|
||||
lastModified: stored.updatedAt,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const err = error as Error & { status?: number }
|
||||
|
||||
if (err.status === 409 || err.message === 'Version conflict') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Version conflict. State was modified by another request.',
|
||||
code: 'VERSION_CONFLICT',
|
||||
},
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
console.error('Failed to save TOM generator state:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to save state' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const tenantId = searchParams.get('tenantId')
|
||||
|
||||
if (!tenantId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'tenantId is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const deleted = await stateStore.delete(tenantId)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
tenantId,
|
||||
deleted,
|
||||
deletedAt: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to delete TOM generator state:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to delete state' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return NextResponse.json(
|
||||
{ status: 'ok' },
|
||||
{
|
||||
headers: {
|
||||
Allow: 'GET, POST, DELETE, OPTIONS',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
40
admin-v2/app/api/sdk/v1/ucca/obligations/assess/route.ts
Normal file
40
admin-v2/app/api/sdk/v1/ucca/obligations/assess/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const SDK_BASE_URL = process.env.SDK_BASE_URL || 'http://localhost:8085'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Forward the request to the SDK backend
|
||||
const response = await fetch(`${SDK_BASE_URL}/sdk/v1/ucca/obligations/assess`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// Forward tenant ID if present
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('SDK backend error:', errorText)
|
||||
return NextResponse.json(
|
||||
{ error: 'SDK backend error', details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Failed to call SDK backend:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to SDK backend' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const SDK_BASE_URL = process.env.SDK_BASE_URL || 'http://localhost:8085'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Forward the request to the SDK backend
|
||||
const response = await fetch(`${SDK_BASE_URL}/sdk/v1/ucca/obligations/export/direct`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// Forward tenant ID if present
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('SDK backend error:', errorText)
|
||||
return NextResponse.json(
|
||||
{ error: 'SDK backend error', details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Failed to call SDK backend:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to SDK backend' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {
|
||||
Finding,
|
||||
CONTRACT_REVIEW_SYSTEM_PROMPT,
|
||||
} from '@/lib/sdk/vendor-compliance'
|
||||
|
||||
/**
|
||||
* POST /api/sdk/v1/vendor-compliance/contracts/[id]/review
|
||||
*
|
||||
* Starts the LLM-based contract review process
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id: contractId } = await params
|
||||
|
||||
// In production:
|
||||
// 1. Fetch contract from database
|
||||
// 2. Extract text from PDF/DOCX using embedding-service
|
||||
// 3. Send to LLM for analysis
|
||||
// 4. Store findings in database
|
||||
// 5. Update contract with compliance score
|
||||
|
||||
// For demo, return mock analysis results
|
||||
const mockFindings: Finding[] = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
tenantId: 'default',
|
||||
contractId,
|
||||
vendorId: 'mock-vendor',
|
||||
type: 'OK',
|
||||
category: 'AVV_CONTENT',
|
||||
severity: 'LOW',
|
||||
title: {
|
||||
de: 'Weisungsgebundenheit vorhanden',
|
||||
en: 'Instruction binding present',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag enthält eine angemessene Regelung zur Weisungsgebundenheit des Auftragsverarbeiters.',
|
||||
en: 'The contract contains an appropriate provision for processor instruction binding.',
|
||||
},
|
||||
citations: [
|
||||
{
|
||||
documentId: contractId,
|
||||
page: 2,
|
||||
startChar: 150,
|
||||
endChar: 350,
|
||||
quotedText: 'Der Auftragnehmer verarbeitet personenbezogene Daten ausschließlich auf dokumentierte Weisung des Auftraggebers.',
|
||||
quoteHash: 'abc123',
|
||||
},
|
||||
],
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. a DSGVO',
|
||||
triggeredControls: ['VND-CON-01'],
|
||||
status: 'OPEN',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
tenantId: 'default',
|
||||
contractId,
|
||||
vendorId: 'mock-vendor',
|
||||
type: 'GAP',
|
||||
category: 'INCIDENT',
|
||||
severity: 'HIGH',
|
||||
title: {
|
||||
de: 'Meldefrist für Datenpannen zu lang',
|
||||
en: 'Data breach notification deadline too long',
|
||||
},
|
||||
description: {
|
||||
de: 'Die vereinbarte Meldefrist von 72 Stunden ist zu lang, um die eigene Meldepflicht gegenüber der Aufsichtsbehörde fristgerecht erfüllen zu können.',
|
||||
en: 'The agreed notification deadline of 72 hours is too long to meet own notification obligations to the supervisory authority in time.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Verhandeln Sie eine kürzere Meldefrist von maximal 24-48 Stunden.',
|
||||
en: 'Negotiate a shorter notification deadline of maximum 24-48 hours.',
|
||||
},
|
||||
citations: [
|
||||
{
|
||||
documentId: contractId,
|
||||
page: 5,
|
||||
startChar: 820,
|
||||
endChar: 950,
|
||||
quotedText: 'Der Auftragnehmer wird den Auftraggeber innerhalb von 72 Stunden über eine Verletzung des Schutzes personenbezogener Daten informieren.',
|
||||
quoteHash: 'def456',
|
||||
},
|
||||
],
|
||||
affectedRequirement: 'Art. 33 Abs. 2 DSGVO',
|
||||
triggeredControls: ['VND-INC-01'],
|
||||
status: 'OPEN',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
tenantId: 'default',
|
||||
contractId,
|
||||
vendorId: 'mock-vendor',
|
||||
type: 'RISK',
|
||||
category: 'TRANSFER',
|
||||
severity: 'MEDIUM',
|
||||
title: {
|
||||
de: 'Drittlandtransfer USA ohne TIA',
|
||||
en: 'Third country transfer to USA without TIA',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag erlaubt Datenverarbeitung in den USA. Es liegt jedoch kein Transfer Impact Assessment (TIA) vor.',
|
||||
en: 'The contract allows data processing in the USA. However, no Transfer Impact Assessment (TIA) is available.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Führen Sie ein TIA durch und dokumentieren Sie zusätzliche Schutzmaßnahmen.',
|
||||
en: 'Conduct a TIA and document supplementary measures.',
|
||||
},
|
||||
citations: [
|
||||
{
|
||||
documentId: contractId,
|
||||
page: 8,
|
||||
startChar: 1200,
|
||||
endChar: 1350,
|
||||
quotedText: 'Die Verarbeitung kann auch in Rechenzentren in den Vereinigten Staaten von Amerika erfolgen.',
|
||||
quoteHash: 'ghi789',
|
||||
},
|
||||
],
|
||||
affectedRequirement: 'Art. 44-49 DSGVO, Schrems II',
|
||||
triggeredControls: ['VND-TRF-01', 'VND-TRF-03'],
|
||||
status: 'OPEN',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
]
|
||||
|
||||
// Calculate compliance score based on findings
|
||||
const okFindings = mockFindings.filter((f) => f.type === 'OK').length
|
||||
const totalChecks = mockFindings.length + 5 // Assume 5 additional checks passed
|
||||
const complianceScore = Math.round((okFindings / totalChecks) * 100 + 60) // Base score + passed checks
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
contractId,
|
||||
findings: mockFindings,
|
||||
complianceScore: Math.min(100, complianceScore),
|
||||
reviewCompletedAt: new Date().toISOString(),
|
||||
topRisks: [
|
||||
{ de: 'Meldefrist für Datenpannen zu lang', en: 'Data breach notification deadline too long' },
|
||||
{ de: 'Fehlende TIA für USA-Transfer', en: 'Missing TIA for USA transfer' },
|
||||
],
|
||||
requiredActions: [
|
||||
{ de: 'Meldefrist auf 24-48h verkürzen', en: 'Reduce notification deadline to 24-48h' },
|
||||
{ de: 'TIA für USA-Transfer durchführen', en: 'Conduct TIA for USA transfer' },
|
||||
],
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error reviewing contract:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to review contract' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sdk/v1/vendor-compliance/contracts/[id]/review
|
||||
*
|
||||
* Get existing review results
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id: contractId } = await params
|
||||
|
||||
// In production, fetch from database
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
contractId,
|
||||
findings: [],
|
||||
complianceScore: null,
|
||||
reviewStatus: 'PENDING',
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching review:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch review' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
88
admin-v2/app/api/sdk/v1/vendor-compliance/contracts/route.ts
Normal file
88
admin-v2/app/api/sdk/v1/vendor-compliance/contracts/route.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ContractDocument } from '@/lib/sdk/vendor-compliance'
|
||||
|
||||
// In-memory storage for demo purposes
|
||||
const contracts: Map<string, ContractDocument> = new Map()
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const contractList = Array.from(contracts.values())
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: contractList,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching contracts:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch contracts' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Handle multipart form data for file upload
|
||||
const formData = await request.formData()
|
||||
const file = formData.get('file') as File | null
|
||||
const vendorId = formData.get('vendorId') as string
|
||||
const metadataStr = formData.get('metadata') as string
|
||||
|
||||
if (!file || !vendorId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'File and vendorId are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const metadata = metadataStr ? JSON.parse(metadataStr) : {}
|
||||
const id = uuidv4()
|
||||
|
||||
// In production, upload file to storage (MinIO, S3, etc.)
|
||||
const storagePath = `contracts/${id}/${file.name}`
|
||||
|
||||
const contract: ContractDocument = {
|
||||
id,
|
||||
tenantId: 'default',
|
||||
vendorId,
|
||||
fileName: `${id}-${file.name}`,
|
||||
originalName: file.name,
|
||||
mimeType: file.type,
|
||||
fileSize: file.size,
|
||||
storagePath,
|
||||
documentType: metadata.documentType || 'OTHER',
|
||||
version: metadata.version || '1.0',
|
||||
previousVersionId: metadata.previousVersionId,
|
||||
parties: metadata.parties,
|
||||
effectiveDate: metadata.effectiveDate ? new Date(metadata.effectiveDate) : undefined,
|
||||
expirationDate: metadata.expirationDate ? new Date(metadata.expirationDate) : undefined,
|
||||
autoRenewal: metadata.autoRenewal,
|
||||
renewalNoticePeriod: metadata.renewalNoticePeriod,
|
||||
terminationNoticePeriod: metadata.terminationNoticePeriod,
|
||||
reviewStatus: 'PENDING',
|
||||
status: 'DRAFT',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
contracts.set(id, contract)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: contract,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
{ status: 201 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error uploading contract:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to upload contract' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
28
admin-v2/app/api/sdk/v1/vendor-compliance/controls/route.ts
Normal file
28
admin-v2/app/api/sdk/v1/vendor-compliance/controls/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { CONTROLS_LIBRARY } from '@/lib/sdk/vendor-compliance'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const domain = searchParams.get('domain')
|
||||
|
||||
let controls = [...CONTROLS_LIBRARY]
|
||||
|
||||
// Filter by domain if provided
|
||||
if (domain) {
|
||||
controls = controls.filter((c) => c.domain === domain)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: controls,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching controls:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch controls' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* GET /api/sdk/v1/vendor-compliance/export/[reportId]/download
|
||||
*
|
||||
* Download a generated report file.
|
||||
* In production, this would redirect to a signed MinIO/S3 URL or stream the file.
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ reportId: string }> }
|
||||
) {
|
||||
const { reportId } = await params
|
||||
|
||||
// TODO: Implement actual file download
|
||||
// This would typically:
|
||||
// 1. Verify report exists and user has access
|
||||
// 2. Generate signed URL for MinIO/S3
|
||||
// 3. Redirect to signed URL or stream file
|
||||
|
||||
// For now, return a placeholder PDF
|
||||
const placeholderContent = `
|
||||
%PDF-1.4
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Length 200 >>
|
||||
stream
|
||||
BT
|
||||
/F1 24 Tf
|
||||
100 700 Td
|
||||
(Vendor Compliance Report) Tj
|
||||
/F1 12 Tf
|
||||
100 650 Td
|
||||
(Report ID: ${reportId}) Tj
|
||||
100 620 Td
|
||||
(Generated: ${new Date().toISOString()}) Tj
|
||||
100 580 Td
|
||||
(This is a placeholder. Implement actual report generation.) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000058 00000 n
|
||||
0000000115 00000 n
|
||||
0000000266 00000 n
|
||||
0000000519 00000 n
|
||||
trailer
|
||||
<< /Size 6 /Root 1 0 R >>
|
||||
startxref
|
||||
598
|
||||
%%EOF
|
||||
`.trim()
|
||||
|
||||
// Return as PDF
|
||||
return new NextResponse(placeholderContent, {
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="Report_${reportId.slice(0, 8)}.pdf"`,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
/**
|
||||
* GET /api/sdk/v1/vendor-compliance/export/[reportId]
|
||||
*
|
||||
* Get report metadata by ID.
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ reportId: string }> }
|
||||
) {
|
||||
const { reportId } = await params
|
||||
|
||||
// TODO: Fetch report metadata from database
|
||||
// For now, return mock data
|
||||
|
||||
return NextResponse.json({
|
||||
id: reportId,
|
||||
status: 'completed',
|
||||
filename: `Report_${reportId.slice(0, 8)}.pdf`,
|
||||
generatedAt: new Date().toISOString(),
|
||||
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24h
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/sdk/v1/vendor-compliance/export/[reportId]
|
||||
*
|
||||
* Delete a generated report.
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ reportId: string }> }
|
||||
) {
|
||||
const { reportId } = await params
|
||||
|
||||
// TODO: Delete report from storage and database
|
||||
console.log('Deleting report:', reportId)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
deletedId: reportId,
|
||||
})
|
||||
}
|
||||
118
admin-v2/app/api/sdk/v1/vendor-compliance/export/route.ts
Normal file
118
admin-v2/app/api/sdk/v1/vendor-compliance/export/route.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
/**
|
||||
* POST /api/sdk/v1/vendor-compliance/export
|
||||
*
|
||||
* Generate and export reports in various formats.
|
||||
* Currently returns mock data - integrate with actual report generation service.
|
||||
*/
|
||||
|
||||
interface ExportConfig {
|
||||
reportType: 'VVT_EXPORT' | 'VENDOR_AUDIT' | 'ROPA' | 'MANAGEMENT_SUMMARY' | 'DPIA_INPUT'
|
||||
format: 'PDF' | 'DOCX' | 'XLSX' | 'JSON'
|
||||
scope: {
|
||||
vendorIds: string[]
|
||||
processingActivityIds: string[]
|
||||
includeFindings: boolean
|
||||
includeControls: boolean
|
||||
includeRiskAssessment: boolean
|
||||
dateRange?: {
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const REPORT_TYPE_NAMES: Record<ExportConfig['reportType'], string> = {
|
||||
VVT_EXPORT: 'Verarbeitungsverzeichnis',
|
||||
VENDOR_AUDIT: 'Vendor-Audit-Pack',
|
||||
ROPA: 'RoPA',
|
||||
MANAGEMENT_SUMMARY: 'Management-Summary',
|
||||
DPIA_INPUT: 'DSFA-Input',
|
||||
}
|
||||
|
||||
const FORMAT_EXTENSIONS: Record<ExportConfig['format'], string> = {
|
||||
PDF: 'pdf',
|
||||
DOCX: 'docx',
|
||||
XLSX: 'xlsx',
|
||||
JSON: 'json',
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const config = (await request.json()) as ExportConfig
|
||||
|
||||
// Validate request
|
||||
if (!config.reportType || !config.format) {
|
||||
return NextResponse.json(
|
||||
{ error: 'reportType and format are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate report ID and filename
|
||||
const reportId = uuidv4()
|
||||
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '')
|
||||
const filename = `${REPORT_TYPE_NAMES[config.reportType]}_${timestamp}.${FORMAT_EXTENSIONS[config.format]}`
|
||||
|
||||
// TODO: Implement actual report generation
|
||||
// This would typically:
|
||||
// 1. Fetch data from database based on scope
|
||||
// 2. Generate report using template engine (e.g., docx-templates, pdfkit)
|
||||
// 3. Store in MinIO/S3
|
||||
// 4. Return download URL
|
||||
|
||||
// Mock implementation - simulate processing time
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// In production, this would be a signed URL to MinIO/S3
|
||||
const downloadUrl = `/api/sdk/v1/vendor-compliance/export/${reportId}/download`
|
||||
|
||||
// Log export for audit trail
|
||||
console.log('Export generated:', {
|
||||
reportId,
|
||||
reportType: config.reportType,
|
||||
format: config.format,
|
||||
scope: config.scope,
|
||||
filename,
|
||||
generatedAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
id: reportId,
|
||||
reportType: config.reportType,
|
||||
format: config.format,
|
||||
filename,
|
||||
downloadUrl,
|
||||
generatedAt: new Date().toISOString(),
|
||||
scope: {
|
||||
vendorCount: config.scope.vendorIds?.length || 0,
|
||||
activityCount: config.scope.processingActivityIds?.length || 0,
|
||||
includesFindings: config.scope.includeFindings,
|
||||
includesControls: config.scope.includeControls,
|
||||
includesRiskAssessment: config.scope.includeRiskAssessment,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Export error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate export' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sdk/v1/vendor-compliance/export
|
||||
*
|
||||
* List recent exports for the current tenant.
|
||||
*/
|
||||
export async function GET() {
|
||||
// TODO: Implement fetching recent exports from database
|
||||
// For now, return empty list
|
||||
return NextResponse.json({
|
||||
exports: [],
|
||||
totalCount: 0,
|
||||
})
|
||||
}
|
||||
43
admin-v2/app/api/sdk/v1/vendor-compliance/findings/route.ts
Normal file
43
admin-v2/app/api/sdk/v1/vendor-compliance/findings/route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { Finding } from '@/lib/sdk/vendor-compliance'
|
||||
|
||||
// In-memory storage for demo purposes
|
||||
const findings: Map<string, Finding> = new Map()
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const vendorId = searchParams.get('vendorId')
|
||||
const contractId = searchParams.get('contractId')
|
||||
const status = searchParams.get('status')
|
||||
|
||||
let findingsList = Array.from(findings.values())
|
||||
|
||||
// Filter by vendor
|
||||
if (vendorId) {
|
||||
findingsList = findingsList.filter((f) => f.vendorId === vendorId)
|
||||
}
|
||||
|
||||
// Filter by contract
|
||||
if (contractId) {
|
||||
findingsList = findingsList.filter((f) => f.contractId === contractId)
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if (status) {
|
||||
findingsList = findingsList.filter((f) => f.status === status)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: findingsList,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching findings:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch findings' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
// This would reference the same storage as the main route
|
||||
// In production, this would be database calls
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
// In production, fetch from database
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: null, // Would return the activity
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching processing activity:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch processing activity' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
|
||||
// In production, update in database
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { id, ...body, updatedAt: new Date() },
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error updating processing activity:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to update processing activity' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
// In production, delete from database
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error deleting processing activity:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to delete processing activity' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ProcessingActivity, generateVVTId } from '@/lib/sdk/vendor-compliance'
|
||||
|
||||
// In-memory storage for demo purposes
|
||||
// In production, this would be replaced with database calls
|
||||
const processingActivities: Map<string, ProcessingActivity> = new Map()
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const activities = Array.from(processingActivities.values())
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: activities,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching processing activities:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch processing activities' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Generate IDs
|
||||
const id = uuidv4()
|
||||
const existingIds = Array.from(processingActivities.values()).map((a) => a.vvtId)
|
||||
const vvtId = body.vvtId || generateVVTId(existingIds)
|
||||
|
||||
const activity: ProcessingActivity = {
|
||||
id,
|
||||
tenantId: 'default', // Would come from auth context
|
||||
vvtId,
|
||||
name: body.name,
|
||||
responsible: body.responsible,
|
||||
dpoContact: body.dpoContact,
|
||||
purposes: body.purposes || [],
|
||||
dataSubjectCategories: body.dataSubjectCategories || [],
|
||||
personalDataCategories: body.personalDataCategories || [],
|
||||
recipientCategories: body.recipientCategories || [],
|
||||
thirdCountryTransfers: body.thirdCountryTransfers || [],
|
||||
retentionPeriod: body.retentionPeriod || { description: { de: '', en: '' } },
|
||||
technicalMeasures: body.technicalMeasures || [],
|
||||
legalBasis: body.legalBasis || [],
|
||||
dataSources: body.dataSources || [],
|
||||
systems: body.systems || [],
|
||||
dataFlows: body.dataFlows || [],
|
||||
protectionLevel: body.protectionLevel || 'MEDIUM',
|
||||
dpiaRequired: body.dpiaRequired || false,
|
||||
dpiaJustification: body.dpiaJustification,
|
||||
subProcessors: body.subProcessors || [],
|
||||
legalRetentionBasis: body.legalRetentionBasis,
|
||||
status: body.status || 'DRAFT',
|
||||
owner: body.owner || '',
|
||||
lastReviewDate: body.lastReviewDate,
|
||||
nextReviewDate: body.nextReviewDate,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
processingActivities.set(id, activity)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: activity,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
{ status: 201 }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error creating processing activity:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to create processing activity' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user