Aus interner Sicherheits-Review (verifiziert): - Rapporte (GET+POST /api/rapports): Tenant-Check via getProjectWithTenantCheck ergänzt — vorher konnte jeder eingeloggte Nutzer mit fremder projectId Rapporte fremder Mandanten auflisten (inkl. Token → voller Inhalt) bzw. anlegen. POST blockt jetzt zusätzlich VIEWER. - Plan-Bild (GET .../plan-image/serve): globaler Lookup → getProjectWithTenantCheck. - Journal-Suggestions (GET /api/tenants/[tenantId]/suggestions): Membership-Check ergänzt (kein Cross-Tenant-Lesen mehr). - Admin-User-Anlegen: Nicht-SERVER_ADMIN kann nur noch im EIGENEN Tenant anlegen (mitgeschickte tenantId aus dem Body wird ignoriert) — verhindert Rechte- Ausweitung in fremde Organisationen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
149 lines
5.4 KiB
TypeScript
149 lines
5.4 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { prisma } from '@/lib/db'
|
|
import { getSession, isAdmin } from '@/lib/auth'
|
|
import { getProjectWithTenantCheck } from '@/lib/tenant'
|
|
import QRCode from 'qrcode'
|
|
|
|
// Helper: create a rapport record and return JSON response
|
|
async function createRapport(projectId: string, data: any, tenantId: string, userId: string, req: NextRequest) {
|
|
// Einsatz-Nummer: one unique number per project (not per rapport)
|
|
const existingRapport = await (prisma as any).rapport.findFirst({
|
|
where: { projectId },
|
|
orderBy: { createdAt: 'asc' },
|
|
select: { reportNumber: true },
|
|
})
|
|
|
|
let reportNumber: string
|
|
if (existingRapport?.reportNumber) {
|
|
reportNumber = existingRapport.reportNumber
|
|
} else {
|
|
const year = new Date().getFullYear()
|
|
const prefix = `${year}-`
|
|
const lastRapport = await (prisma as any).rapport.findFirst({
|
|
where: { tenantId, reportNumber: { startsWith: prefix } },
|
|
orderBy: { reportNumber: 'desc' },
|
|
select: { reportNumber: true },
|
|
})
|
|
let nextNum = 1
|
|
if (lastRapport?.reportNumber) {
|
|
const lastNum = parseInt(lastRapport.reportNumber.split('-')[1])
|
|
if (!isNaN(lastNum)) nextNum = lastNum + 1
|
|
}
|
|
reportNumber = `${prefix}${String(nextNum).padStart(4, '0')}`
|
|
}
|
|
|
|
data.reportNumber = reportNumber
|
|
|
|
const rapport = await (prisma as any).rapport.create({
|
|
data: { reportNumber, data, projectId, tenantId, createdById: userId },
|
|
})
|
|
|
|
// Generate QR code
|
|
const baseUrl = process.env.NEXTAUTH_URL || req.headers.get('origin') || `${req.headers.get('x-forwarded-proto') || 'https'}://${req.headers.get('host')}` || 'http://localhost:3000'
|
|
const rapportUrl = `${baseUrl}/rapport/${rapport.token}`
|
|
let qrCodeDataUri = ''
|
|
try {
|
|
qrCodeDataUri = await QRCode.toDataURL(rapportUrl, { width: 200, margin: 1, color: { dark: '#1a1a1a', light: '#ffffff' } })
|
|
} catch (e) {
|
|
console.warn('QR code generation failed:', e)
|
|
}
|
|
|
|
if (qrCodeDataUri) {
|
|
data.qrCodeUrl = qrCodeDataUri
|
|
await (prisma as any).rapport.update({ where: { id: rapport.id }, data: { data } })
|
|
}
|
|
|
|
return NextResponse.json({
|
|
id: rapport.id,
|
|
reportNumber: rapport.reportNumber,
|
|
token: rapport.token,
|
|
qrCodeUrl: qrCodeDataUri,
|
|
})
|
|
}
|
|
|
|
// GET: List rapports for a project
|
|
export async function GET(req: NextRequest) {
|
|
try {
|
|
const user = await getSession()
|
|
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
|
|
|
const { searchParams } = new URL(req.url)
|
|
const projectId = searchParams.get('projectId')
|
|
|
|
if (!projectId) {
|
|
return NextResponse.json({ error: 'projectId erforderlich' }, { status: 400 })
|
|
}
|
|
|
|
// Mandanten-Isolation: nur Rapporte von Projekten des eigenen Tenants
|
|
const project = await getProjectWithTenantCheck(projectId, user)
|
|
if (!project) {
|
|
return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
|
}
|
|
|
|
const rapports = await (prisma as any).rapport.findMany({
|
|
where: { projectId },
|
|
orderBy: { createdAt: 'desc' },
|
|
select: {
|
|
id: true,
|
|
reportNumber: true,
|
|
token: true,
|
|
generatedAt: true,
|
|
createdAt: true,
|
|
createdBy: { select: { name: true } },
|
|
},
|
|
})
|
|
|
|
return NextResponse.json({ rapports })
|
|
} catch (error) {
|
|
console.error('Error fetching rapports:', error)
|
|
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
|
}
|
|
}
|
|
|
|
// POST: Create a new rapport
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const user = await getSession()
|
|
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
|
if (user.role === 'VIEWER') return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
|
|
|
let body: any
|
|
try {
|
|
body = await req.json()
|
|
} catch (parseErr: any) {
|
|
console.error('[Rapport] Body parse error:', parseErr?.message)
|
|
return NextResponse.json({ error: 'Request zu gross oder ungültig: ' + (parseErr?.message || 'Body konnte nicht gelesen werden') }, { status: 400 })
|
|
}
|
|
const { projectId, data } = body || {}
|
|
|
|
if (!projectId || !data) {
|
|
console.error('[Rapport] Missing fields — projectId:', !!projectId, 'data:', !!data, 'body keys:', Object.keys(body || {}))
|
|
return NextResponse.json({ error: `Felder fehlen (projectId=${!!projectId}, data=${!!data})` }, { status: 400 })
|
|
}
|
|
|
|
// Mandanten-Isolation: Rapport nur für ein Projekt des eigenen Tenants anlegen
|
|
const project = await getProjectWithTenantCheck(projectId, user)
|
|
if (!project) {
|
|
return NextResponse.json({ error: 'Projekt nicht gefunden' }, { status: 404 })
|
|
}
|
|
|
|
let tenantId = project?.tenantId || user.tenantId || null
|
|
if (!tenantId) {
|
|
const membership = await (prisma as any).tenantMembership.findFirst({
|
|
where: { userId: user.id },
|
|
select: { tenantId: true },
|
|
})
|
|
tenantId = membership?.tenantId || null
|
|
}
|
|
|
|
console.log('[Rapport] Creating rapport — project:', projectId, 'tenant:', tenantId || '(none)')
|
|
return await createRapport(projectId, data, tenantId, user.id, req)
|
|
} catch (error: any) {
|
|
console.error('[Rapport] Error:', error?.message || error)
|
|
if (error?.code === 'P2002') {
|
|
return NextResponse.json({ error: 'Rapportnummer existiert bereits' }, { status: 409 })
|
|
}
|
|
return NextResponse.json({ error: error?.message || 'Serverfehler' }, { status: 500 })
|
|
}
|
|
}
|