205 lines
9.5 KiB
TypeScript
205 lines
9.5 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { prisma } from '@/lib/db'
|
|
import { getSession } from '@/lib/auth'
|
|
import { sendEmail, getSmtpConfig } from '@/lib/email'
|
|
import { z } from 'zod'
|
|
|
|
const upgradeSchema = z.object({
|
|
requestedPlan: z.enum(['PRO']),
|
|
message: z.string().max(1000).optional(),
|
|
})
|
|
|
|
// GET: List upgrade requests for current tenant (TENANT_ADMIN) or all (SERVER_ADMIN)
|
|
export async function GET() {
|
|
try {
|
|
const user = await getSession()
|
|
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
|
|
|
let where: any = {}
|
|
if (user.role === 'SERVER_ADMIN') {
|
|
// Server admin sees all
|
|
} else if (user.role === 'TENANT_ADMIN' && user.tenantId) {
|
|
where = { tenantId: user.tenantId }
|
|
} else {
|
|
return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 })
|
|
}
|
|
|
|
const requests = await (prisma as any).upgradeRequest.findMany({
|
|
where,
|
|
include: {
|
|
tenant: { select: { name: true, slug: true, plan: true, subscriptionStatus: true } },
|
|
requestedBy: { select: { name: true, email: true } },
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
})
|
|
|
|
return NextResponse.json({ requests })
|
|
} catch (error) {
|
|
console.error('Error fetching upgrade requests:', error)
|
|
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
|
}
|
|
}
|
|
|
|
// POST: Create a new upgrade request (TENANT_ADMIN only)
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const user = await getSession()
|
|
if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
|
|
if (user.role !== 'TENANT_ADMIN' || !user.tenantId) {
|
|
return NextResponse.json({ error: 'Nur Mandanten-Administratoren können Upgrades anfordern' }, { status: 403 })
|
|
}
|
|
|
|
const body = await req.json()
|
|
const validated = upgradeSchema.safeParse(body)
|
|
if (!validated.success) {
|
|
return NextResponse.json({ error: 'Ungültige Eingabe', details: validated.error.flatten() }, { status: 400 })
|
|
}
|
|
|
|
// Get current tenant
|
|
const tenant = await (prisma as any).tenant.findUnique({
|
|
where: { id: user.tenantId },
|
|
select: { id: true, name: true, plan: true, contactEmail: true },
|
|
})
|
|
if (!tenant) {
|
|
return NextResponse.json({ error: 'Mandant nicht gefunden' }, { status: 404 })
|
|
}
|
|
|
|
// Check for existing pending request
|
|
const existingPending = await (prisma as any).upgradeRequest.findFirst({
|
|
where: { tenantId: user.tenantId, status: 'PENDING' },
|
|
})
|
|
if (existingPending) {
|
|
return NextResponse.json({
|
|
error: 'Es gibt bereits eine offene Upgrade-Anfrage. Bitte warten Sie auf die Bearbeitung.',
|
|
}, { status: 409 })
|
|
}
|
|
|
|
// Don't allow "downgrade" requests or same plan
|
|
const planOrder = { FREE: 0, PRO: 1 }
|
|
if ((planOrder[validated.data.requestedPlan as keyof typeof planOrder] || 0) <= (planOrder[tenant.plan as keyof typeof planOrder] || 0)) {
|
|
return NextResponse.json({ error: 'Der gewählte Plan ist kein Upgrade gegenüber dem aktuellen Plan.' }, { status: 400 })
|
|
}
|
|
|
|
// Create request
|
|
const request = await (prisma as any).upgradeRequest.create({
|
|
data: {
|
|
tenantId: user.tenantId,
|
|
requestedById: user.id,
|
|
requestedPlan: validated.data.requestedPlan,
|
|
currentPlan: tenant.plan,
|
|
message: validated.data.message || null,
|
|
},
|
|
include: {
|
|
tenant: { select: { name: true } },
|
|
requestedBy: { select: { name: true, email: true } },
|
|
},
|
|
})
|
|
|
|
// Send emails
|
|
const smtpConfig = await getSmtpConfig()
|
|
if (smtpConfig) {
|
|
const planLabels: Record<string, string> = {
|
|
FREE: 'Free', PRO: 'Pro',
|
|
}
|
|
|
|
// 1. Confirmation to tenant admin
|
|
try {
|
|
await sendEmail(
|
|
user.email,
|
|
`Upgrade-Anfrage bestätigt — ${planLabels[validated.data.requestedPlan]}`,
|
|
`
|
|
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 520px; margin: 0 auto;">
|
|
<div style="background: #2563eb; color: white; padding: 16px 24px; border-radius: 8px 8px 0 0;">
|
|
<h2 style="margin: 0; font-size: 18px;">Upgrade-Anfrage eingegangen</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>${tenant.name}</strong> wurde erfolgreich übermittelt.
|
|
</p>
|
|
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Aktueller Plan</td>
|
|
<td style="padding: 8px 0; font-weight: 600; text-align: right;">${planLabels[tenant.plan] || tenant.plan}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Gewünschter Plan</td>
|
|
<td style="padding: 8px 0; font-weight: 600; text-align: right; color: #2563eb;">${planLabels[validated.data.requestedPlan]}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Status</td>
|
|
<td style="padding: 8px 0; font-weight: 600; text-align: right;">
|
|
<span style="background: #fef3c7; color: #92400e; padding: 2px 8px; border-radius: 4px; font-size: 12px;">Wird geprüft</span>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
${validated.data.message ? `<p style="margin: 16px 0 0; padding: 12px; background: #f9fafb; border-radius: 6px; font-size: 14px; color: #374151;"><strong>Ihre Nachricht:</strong><br/>${validated.data.message.replace(/\n/g, '<br/>')}</p>` : ''}
|
|
<p style="margin: 16px 0 0; font-size: 13px; color: #6b7280;">
|
|
Wir werden Ihre Anfrage so schnell wie möglich bearbeiten. Sie erhalten eine Benachrichtigung, sobald Ihr Plan aktiviert wurde.
|
|
</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 upgrade confirmation email:', e)
|
|
}
|
|
|
|
// 2. Notification to all server admins
|
|
try {
|
|
const serverAdmins = await (prisma as any).user.findMany({
|
|
where: { role: 'SERVER_ADMIN' },
|
|
select: { email: true, name: true },
|
|
})
|
|
for (const admin of serverAdmins) {
|
|
await sendEmail(
|
|
admin.email,
|
|
`Neue Upgrade-Anfrage: ${tenant.name} → ${planLabels[validated.data.requestedPlan]}`,
|
|
`
|
|
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 520px; margin: 0 auto;">
|
|
<div style="background: #dc2626; color: white; padding: 16px 24px; border-radius: 8px 8px 0 0;">
|
|
<h2 style="margin: 0; font-size: 18px;">Neue Upgrade-Anfrage</h2>
|
|
</div>
|
|
<div style="border: 1px solid #e5e7eb; border-top: none; padding: 24px; border-radius: 0 0 8px 8px;">
|
|
<table style="width: 100%; border-collapse: collapse; margin: 0 0 16px;">
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Organisation</td>
|
|
<td style="padding: 8px 0; font-weight: 600; text-align: right;">${tenant.name}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Angefragt von</td>
|
|
<td style="padding: 8px 0; font-weight: 600; text-align: right;">${user.name} (${user.email})</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Aktueller Plan</td>
|
|
<td style="padding: 8px 0; text-align: right;">${planLabels[tenant.plan] || tenant.plan}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; color: #6b7280; font-size: 14px;">Gewünschter Plan</td>
|
|
<td style="padding: 8px 0; font-weight: 600; text-align: right; color: #2563eb;">${planLabels[validated.data.requestedPlan]}</td>
|
|
</tr>
|
|
</table>
|
|
${validated.data.message ? `<p style="margin: 0 0 16px; padding: 12px; background: #f9fafb; border-radius: 6px; font-size: 14px; color: #374151;"><strong>Nachricht:</strong><br/>${validated.data.message.replace(/\n/g, '<br/>')}</p>` : ''}
|
|
<p style="margin: 0; font-size: 13px; color: #6b7280;">
|
|
Bitte prüfen und bestätigen Sie die Anfrage im Admin-Panel unter "Upgrade-Anfragen".
|
|
</p>
|
|
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;" />
|
|
<p style="margin: 0; font-size: 12px; color: #9ca3af;">Lageplan — Automatische Benachrichtigung</p>
|
|
</div>
|
|
</div>
|
|
`
|
|
)
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to send admin notification email:', e)
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({ request }, { status: 201 })
|
|
} catch (error) {
|
|
console.error('Error creating upgrade request:', error)
|
|
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
|
|
}
|
|
}
|