Scrum4Me/actions/auth.ts
janpeter visser d11b114fc1 feat: ST-601-ST-612 M6 polish, beveiliging en launch-ready
- ST-601/602: loading skeletons en error boundary
- ST-603: Sonner toasts op alle CRUD-operaties
- ST-604: DemoTooltip op uitgeschakelde knoppen
- ST-605: KeyboardSensor dnd-kit, Escape sluit modals
- ST-606: min-width banner < 1024px
- ST-607: WCAG AA aria-labels en skip link
- ST-608: rate limiting login (10/min) en registratie (5/uur)
- ST-609: security integratietests cross-user toegang (7 tests)
- ST-610: GitHub Actions CI/CD workflow
- ST-611: README met quickstart, deployment en API-docs
- ST-612: Lars-flow acceptatiechecklist
- fix: settings toont gebruikersnaam i.p.v. interne id
- fix: seed idempotent, testdata altijd gekoppeld aan demo-gebruiker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 12:36:23 +02:00

84 lines
2.5 KiB
TypeScript

'use server'
import { redirect } from 'next/navigation'
import { cookies, headers } from 'next/headers'
import { getIronSession } from 'iron-session'
import { z } from 'zod'
import { registerUser, verifyUser } from '@/lib/auth'
import { SessionData, sessionOptions } from '@/lib/session'
import { checkRateLimit } from '@/lib/rate-limit'
async function getClientIp(): Promise<string> {
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<SessionData>(await cookies(), sessionOptions)
session.userId = result.user!.id
session.isDemo = 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 session = await getIronSession<SessionData>(await cookies(), sessionOptions)
session.userId = user.id
session.isDemo = user.is_demo
await session.save()
redirect('/dashboard')
}
export async function logoutAction() {
const session = await getIronSession<SessionData>(await cookies(), sessionOptions)
session.destroy()
redirect('/login')
}