Ops-dashboard/app/login/page.tsx
Scrum4Me Agent be05724de0 feat: login page, session management, auth API routes en proxy guard
- lib/session.ts: token generatie, SHA-256 hashing, createSession/getCurrentUser/invalidateSession
- app/api/auth/login: bcrypt verificatie, session aanmaken, ops_session cookie (httpOnly, sameSite=strict, 24h TTL), rate-limit 5/min per IP
- app/api/auth/logout: session invalideren en cookie verwijderen
- app/login/page.tsx: login form (client component)
- proxy.ts: route-protectie – redirect naar /login zonder sessie (middleware.ts is deprecated in Next.js 16)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 17:10:07 +02:00

97 lines
3.4 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
export default function LoginPage() {
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setError('')
setLoading(true)
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (res.ok) {
router.push('/')
router.refresh()
} else {
const data = await res.json()
setError(data.error ?? 'Login failed')
}
} catch {
setError('Network error, please try again')
} finally {
setLoading(false)
}
}
return (
<div className="flex min-h-full items-center justify-center bg-zinc-50 dark:bg-zinc-950">
<div className="w-full max-w-sm rounded-xl border border-zinc-200 bg-white p-8 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<h1 className="mb-6 text-xl font-semibold text-zinc-900 dark:text-zinc-50">
Ops Dashboard
</h1>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label
htmlFor="email"
className="text-sm font-medium text-zinc-700 dark:text-zinc-300"
>
Email
</label>
<input
id="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 outline-none placeholder:text-zinc-400 focus:border-zinc-500 focus:ring-2 focus:ring-zinc-500/20 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder:text-zinc-500 dark:focus:border-zinc-400"
placeholder="admin@example.com"
/>
</div>
<div className="flex flex-col gap-1.5">
<label
htmlFor="password"
className="text-sm font-medium text-zinc-700 dark:text-zinc-300"
>
Password
</label>
<input
id="password"
type="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 outline-none placeholder:text-zinc-400 focus:border-zinc-500 focus:ring-2 focus:ring-zinc-500/20 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder:text-zinc-500 dark:focus:border-zinc-400"
placeholder="••••••••"
/>
</div>
{error && (
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
)}
<Button type="submit" disabled={loading} className="mt-2 w-full">
{loading ? 'Signing in…' : 'Sign in'}
</Button>
</form>
</div>
</div>
)
}