fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user