187 lines
8.0 KiB
TypeScript
187 lines
8.0 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { prisma } from '@/lib/db'
|
|
import { getSession, isServerAdmin } from '@/lib/auth'
|
|
import { sendEmail, getSmtpConfig } from '@/lib/email'
|
|
import { z } from 'zod'
|
|
|
|
const processSchema = z.object({
|
|
action: z.enum(['approve', 'reject']),
|
|
adminNote: z.string().max(500).optional(),
|
|
// Only for approve: override limits
|
|
maxUsers: z.number().min(1).max(1000).optional(),
|
|
maxProjects: z.number().min(1).max(10000).optional(),
|
|
})
|
|
|
|
// PATCH: Approve or reject an upgrade request (SERVER_ADMIN only)
|
|
export async function PATCH(
|
|
req: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const { id } = await params
|
|
const user = await getSession()
|
|
if (!user || !isServerAdmin(user.role)) {
|
|
return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 403 })
|
|
}
|
|
|
|
const body = await req.json()
|
|
const validated = processSchema.safeParse(body)
|
|
if (!validated.success) {
|
|
return NextResponse.json({ error: 'Ungültige Eingabe', details: validated.error.flatten() }, { status: 400 })
|
|
}
|
|
|
|
// Get the request
|
|
const upgradeReq = await (prisma as any).upgradeRequest.findUnique({
|
|
where: { id },
|
|
include: {
|
|
tenant: { select: { id: true, name: true, plan: true, contactEmail: true } },
|
|
requestedBy: { select: { name: true, email: true } },
|
|
},
|
|
})
|
|
|
|
if (!upgradeReq) {
|
|
return NextResponse.json({ error: 'Anfrage nicht gefunden' }, { status: 404 })
|
|
}
|
|
|
|
if (upgradeReq.status !== 'PENDING') {
|
|
return NextResponse.json({ error: 'Anfrage wurde bereits bearbeitet' }, { status: 400 })
|
|
}
|
|
|
|
const planLabels: Record<string, string> = {
|
|
FREE: 'Free', PRO: 'Pro',
|
|
}
|
|
const planLimits: Record<string, { maxUsers: number; maxProjects: number }> = {
|
|
FREE: { maxUsers: 5, maxProjects: 10 },
|
|
PRO: { maxUsers: 999, maxProjects: 9999 },
|
|
}
|
|
|
|
if (validated.data.action === 'approve') {
|
|
const limits = planLimits[upgradeReq.requestedPlan] || planLimits.PRO
|
|
|
|
// Update tenant plan
|
|
await (prisma as any).tenant.update({
|
|
where: { id: upgradeReq.tenantId },
|
|
data: {
|
|
plan: upgradeReq.requestedPlan,
|
|
subscriptionStatus: 'ACTIVE',
|
|
trialEndsAt: null,
|
|
maxUsers: validated.data.maxUsers || limits.maxUsers,
|
|
maxProjects: validated.data.maxProjects || limits.maxProjects,
|
|
},
|
|
})
|
|
|
|
// Update request status
|
|
await (prisma as any).upgradeRequest.update({
|
|
where: { id },
|
|
data: {
|
|
status: 'APPROVED',
|
|
adminNote: validated.data.adminNote || null,
|
|
processedAt: new Date(),
|
|
processedById: user.id,
|
|
},
|
|
})
|
|
|
|
// Send approval email to requester
|
|
const smtpConfig = await getSmtpConfig()
|
|
if (smtpConfig) {
|
|
try {
|
|
await sendEmail(
|
|
upgradeReq.requestedBy.email,
|
|
`Upgrade bestätigt — ${planLabels[upgradeReq.requestedPlan]} Plan aktiviert`,
|
|
`
|
|
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 520px; margin: 0 auto;">
|
|
<div style="background: #16a34a; color: white; padding: 16px 24px; border-radius: 8px 8px 0 0;">
|
|
<h2 style="margin: 0; font-size: 18px;">✓ Upgrade bestätigt</h2>
|
|
</div>
|
|
<div style="border: 1px solid #e5e7eb; border-top: none; padding: 24px; border-radius: 0 0 8px 8px;">
|
|
<p style="margin: 0 0 16px; line-height: 1.6; color: #374151;">
|
|
Ihr Upgrade für <strong>${upgradeReq.tenant.name}</strong> wurde bestätigt und ist ab sofort aktiv.
|
|
</p>
|
|
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Neuer Plan</td>
|
|
<td style="padding: 8px 0; font-weight: 600; text-align: right; color: #16a34a;">${planLabels[upgradeReq.requestedPlan]}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Max. Benutzer</td>
|
|
<td style="padding: 8px 0; font-weight: 600; text-align: right;">${validated.data.maxUsers || limits.maxUsers}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Max. Projekte</td>
|
|
<td style="padding: 8px 0; font-weight: 600; text-align: right;">${validated.data.maxProjects || limits.maxProjects}</td>
|
|
</tr>
|
|
</table>
|
|
${validated.data.adminNote ? `<p style="margin: 16px 0 0; padding: 12px; background: #f0fdf4; border-radius: 6px; font-size: 14px; color: #374151;"><strong>Hinweis:</strong><br/>${validated.data.adminNote}</p>` : ''}
|
|
<p style="margin: 16px 0 0; font-size: 13px; color: #6b7280;">
|
|
Vielen Dank für Ihr Vertrauen. Bei Fragen stehen wir Ihnen gerne zur Verfügung.
|
|
</p>
|
|
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;" />
|
|
<p style="margin: 0; font-size: 12px; color: #9ca3af;">Lageplan — Digitale Lagepläne für die Feuerwehr</p>
|
|
</div>
|
|
</div>
|
|
`
|
|
)
|
|
} catch (e) {
|
|
console.error('Failed to send approval email:', e)
|
|
}
|
|
}
|
|
} else {
|
|
// Reject
|
|
await (prisma as any).upgradeRequest.update({
|
|
where: { id },
|
|
data: {
|
|
status: 'REJECTED',
|
|
adminNote: validated.data.adminNote || null,
|
|
processedAt: new Date(),
|
|
processedById: user.id,
|
|
},
|
|
})
|
|
|
|
// Send rejection email
|
|
const smtpConfig = await getSmtpConfig()
|
|
if (smtpConfig) {
|
|
try {
|
|
await sendEmail(
|
|
upgradeReq.requestedBy.email,
|
|
`Upgrade-Anfrage — Rückmeldung`,
|
|
`
|
|
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 520px; margin: 0 auto;">
|
|
<div style="background: #6b7280; color: white; padding: 16px 24px; border-radius: 8px 8px 0 0;">
|
|
<h2 style="margin: 0; font-size: 18px;">Upgrade-Anfrage</h2>
|
|
</div>
|
|
<div style="border: 1px solid #e5e7eb; border-top: none; padding: 24px; border-radius: 0 0 8px 8px;">
|
|
<p style="margin: 0 0 16px; line-height: 1.6; color: #374151;">
|
|
Ihre Upgrade-Anfrage für <strong>${upgradeReq.tenant.name}</strong> auf den <strong>${planLabels[upgradeReq.requestedPlan]}</strong>-Plan konnte leider nicht bestätigt werden.
|
|
</p>
|
|
${validated.data.adminNote ? `<p style="margin: 0 0 16px; padding: 12px; background: #f9fafb; border-radius: 6px; font-size: 14px; color: #374151;"><strong>Begründung:</strong><br/>${validated.data.adminNote}</p>` : ''}
|
|
<p style="margin: 0; font-size: 13px; color: #6b7280;">
|
|
Bei Fragen kontaktieren Sie uns bitte unter app@lageplan.ch. Sie können jederzeit eine neue Anfrage stellen.
|
|
</p>
|
|
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;" />
|
|
<p style="margin: 0; font-size: 12px; color: #9ca3af;">Lageplan — Digitale Lagepläne für die Feuerwehr</p>
|
|
</div>
|
|
</div>
|
|
`
|
|
)
|
|
} catch (e) {
|
|
console.error('Failed to send rejection email:', e)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return updated request
|
|
const updated = await (prisma as any).upgradeRequest.findUnique({
|
|
where: { id },
|
|
include: {
|
|
tenant: { select: { name: true, slug: true, plan: true, subscriptionStatus: true } },
|
|
requestedBy: { select: { name: true, email: true } },
|
|
},
|
|
})
|
|
|
|
return NextResponse.json({ request: updated })
|
|
} catch (error) {
|
|
console.error('Error processing upgrade request:', error)
|
|
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
|
}
|
|
}
|