v1.0.3: PDF footer fix, arrow alignment, email verification workflow, account deletion

This commit is contained in:
Pepe Ziberi
2026-02-21 16:45:44 +01:00
parent 25d3d553ff
commit b75bf9bb30
7 changed files with 278 additions and 6 deletions

View File

@@ -0,0 +1,65 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { getSession } from '@/lib/auth'
import bcrypt from 'bcryptjs'
import { cookies } from 'next/headers'
// POST: User deletes their own account
export async function POST(req: NextRequest) {
try {
const session = await getSession()
if (!session) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
const { password } = await req.json()
if (!password) {
return NextResponse.json({ error: 'Passwort erforderlich' }, { status: 400 })
}
// Verify password
const user = await (prisma as any).user.findUnique({
where: { id: session.id },
select: { id: true, password: true, role: true },
})
if (!user) return NextResponse.json({ error: 'Benutzer nicht gefunden' }, { status: 404 })
const validPw = await bcrypt.compare(password, user.password)
if (!validPw) {
return NextResponse.json({ error: 'Falsches Passwort' }, { status: 403 })
}
// If user is the only TENANT_ADMIN, they must delete the org first or transfer ownership
if (session.tenantId && session.role === 'TENANT_ADMIN') {
const adminCount = await (prisma as any).tenantMembership.count({
where: { tenantId: session.tenantId, role: 'TENANT_ADMIN' },
})
if (adminCount <= 1) {
return NextResponse.json({
error: 'Sie sind der einzige Administrator. Bitte löschen Sie die Organisation unter Einstellungen oder übertragen Sie die Admin-Rolle.',
}, { status: 400 })
}
}
console.log(`[Account Delete] User ${session.id} (${session.email}) deleting own account`)
// Clean up user data
try { await (prisma as any).upgradeRequest.deleteMany({ where: { requestedById: session.id } }) } catch {}
try { await (prisma as any).iconAsset.updateMany({ where: { ownerId: session.id }, data: { ownerId: null } }) } catch {}
try { await (prisma as any).project.updateMany({ where: { ownerId: session.id }, data: { ownerId: null } }) } catch {}
// Remove memberships
await (prisma as any).tenantMembership.deleteMany({ where: { userId: session.id } })
// Delete user
await (prisma as any).user.delete({ where: { id: session.id } })
// Clear auth cookie
;(await cookies()).delete('auth-token')
console.log(`[Account Delete] User ${session.email} deleted successfully`)
return NextResponse.json({ success: true, message: 'Konto wurde gelöscht' })
} catch (error: any) {
console.error('[Account Delete] Error:', error?.message || error)
return NextResponse.json({ error: 'Löschung fehlgeschlagen' }, { status: 500 })
}
}

View File

@@ -0,0 +1,70 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { sendEmail } from '@/lib/email'
import { randomBytes } from 'crypto'
export async function POST(req: NextRequest) {
try {
const { email } = await req.json()
if (!email) {
return NextResponse.json({ error: 'E-Mail-Adresse erforderlich.' }, { status: 400 })
}
const user = await (prisma as any).user.findUnique({
where: { email },
include: { memberships: { include: { tenant: true } } },
})
if (!user) {
// Don't reveal whether user exists
return NextResponse.json({ success: true, message: 'Falls ein Konto mit dieser E-Mail existiert, wurde eine neue Bestätigungsmail gesendet.' })
}
if (user.emailVerified) {
return NextResponse.json({ success: true, message: 'Ihre E-Mail-Adresse ist bereits bestätigt. Sie können sich anmelden.' })
}
// Generate new verification token
const verificationToken = randomBytes(32).toString('hex')
await (prisma as any).user.update({
where: { id: user.id },
data: { emailVerificationToken: verificationToken },
})
// Build verification URL
let baseUrl = process.env.NEXTAUTH_URL || req.headers.get('origin') || `${req.headers.get('x-forwarded-proto') || 'https'}://${req.headers.get('host')}` || 'http://localhost:3000'
if (baseUrl && !baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {
baseUrl = `https://${baseUrl}`
}
const verifyUrl = `${baseUrl}/api/auth/verify-email?token=${verificationToken}`
const orgName = user.memberships?.[0]?.tenant?.name || 'Lageplan'
await sendEmail(
user.email,
'E-Mail-Adresse bestätigen — Lageplan',
`<div style="font-family:sans-serif;max-width:600px;margin:0 auto;">
<div style="background:#dc2626;color:white;padding:20px 24px;border-radius:12px 12px 0 0;">
<h1 style="margin:0;font-size:22px;">E-Mail bestätigen</h1>
</div>
<div style="border:1px solid #e5e7eb;border-top:none;padding:24px;border-radius:0 0 12px 12px;">
<p>Hallo <strong>${user.name}</strong>,</p>
<p>Bitte bestätigen Sie Ihre E-Mail-Adresse, um Ihr Konto für <strong>${orgName}</strong> zu aktivieren.</p>
<div style="text-align:center;margin:24px 0;">
<a href="${verifyUrl}" style="background:#dc2626;color:white;padding:12px 32px;text-decoration:none;border-radius:8px;font-weight:600;display:inline-block;">
E-Mail bestätigen
</a>
</div>
<p style="color:#666;font-size:13px;">Falls der Button nicht funktioniert, kopieren Sie diesen Link:<br/>
<a href="${verifyUrl}" style="word-break:break-all;">${verifyUrl}</a></p>
</div>
</div>`
)
return NextResponse.json({ success: true, message: 'Bestätigungsmail wurde erneut gesendet. Bitte prüfen Sie Ihren Posteingang.' })
} catch (error) {
console.error('Resend verification error:', error)
return NextResponse.json({ error: 'Fehler beim Senden der Bestätigungsmail.' }, { status: 500 })
}
}