'use server' import { redirect } from 'next/navigation' import { cookies, headers } from 'next/headers' import { getIronSession } from 'iron-session' import { z } from 'zod' import { prisma } from '@/lib/prisma' import { registerUser, verifyUser, hashPassword } from '@/lib/auth' import { SessionData, sessionOptions } from '@/lib/session' import { checkRateLimit } from '@/lib/rate-limit' import { isPhoneUA } from '@/lib/user-agent' import { requireSession } from '@/lib/auth-guard' async function getClientIp(): Promise { const h = await headers() return h.get('x-forwarded-for')?.split(',')[0].trim() ?? h.get('x-real-ip') ?? 'unknown' } const registerSchema = z.object({ username: z.string().min(3, 'Gebruikersnaam moet minimaal 3 tekens bevatten').max(50), password: z.string().min(8, 'Wachtwoord moet minimaal 8 tekens bevatten'), }) const loginSchema = z.object({ username: z.string().min(1), password: z.string().min(1), }) export async function registerAction(_prevState: unknown, formData: FormData) { const ip = await getClientIp() if (!checkRateLimit(`register:${ip}`)) { return { error: 'Te veel pogingen. Probeer het over een minuut opnieuw.' } } const parsed = registerSchema.safeParse({ username: formData.get('username'), password: formData.get('password'), }) if (!parsed.success) { return { error: parsed.error.flatten().fieldErrors } } const result = await registerUser(parsed.data.username, parsed.data.password) if (result.error) return { error: result.error } const session = await getIronSession(await cookies(), sessionOptions) session.userId = result.user!.id session.isDemo = false session.isAdmin = false await session.save() redirect('/dashboard') } export async function loginAction(_prevState: unknown, formData: FormData) { const ip = await getClientIp() if (!checkRateLimit(`login:${ip}`)) { return { error: 'Te veel inlogpogingen. Probeer het over een minuut opnieuw.' } } const parsed = loginSchema.safeParse({ username: formData.get('username'), password: formData.get('password'), }) if (!parsed.success) { return { error: 'Ongeldige inloggegevens' } } const user = await verifyUser(parsed.data.username, parsed.data.password) if (!user) { return { error: 'Onjuiste gebruikersnaam of wachtwoord' } } const adminRole = await prisma.userRole.findFirst({ where: { user_id: user.id, role: 'ADMIN' }, }) const session = await getIronSession(await cookies(), sessionOptions) session.userId = user.id session.isDemo = user.is_demo session.isAdmin = !!adminRole await session.save() // PBI-11 / ST-1135: telefoon-UA's krijgen de mobile-shell. // Tablets en desktop volgen het bestaande /dashboard-pad. const ua = (await headers()).get('user-agent') if (isPhoneUA(ua)) { redirect(user.active_product_id ? `/m/products/${user.active_product_id}/solo` : '/m/settings') } redirect('/dashboard') } export async function logoutAction() { const session = await getIronSession(await cookies(), sessionOptions) session.destroy() redirect('/login') } const resetPasswordSchema = z .object({ password: z.string().min(8, 'Wachtwoord moet minimaal 8 tekens bevatten'), confirm: z.string(), }) .superRefine((data, ctx) => { if (data.password !== data.confirm) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Wachtwoorden komen niet overeen', path: ['confirm'], }) } }) export async function resetPasswordAction(_prevState: unknown, formData: FormData) { const session = await requireSession() const parsed = resetPasswordSchema.safeParse({ password: formData.get('password'), confirm: formData.get('confirm'), }) if (!parsed.success) { return { error: parsed.error.flatten().fieldErrors } } const hash = await hashPassword(parsed.data.password) await prisma.user.update({ where: { id: session.userId }, data: { password_hash: hash, must_reset_password: false }, }) redirect('/dashboard') }