Initial commit: Lageplan v1.0 - Next.js 15.5, React 19
This commit is contained in:
185
src/app/api/upgrade-requests/[id]/route.ts
Normal file
185
src/app/api/upgrade-requests/[id]/route.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
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: { id: string } }
|
||||
) {
|
||||
try {
|
||||
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: params.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: params.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: params.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: params.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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user