cms.c2sgmbh/src/app/(payload)/admin/login/page.tsx
Martin Porwoll 63b97c14f2 feat(security): enhance CSRF, IP allowlist, and rate limiter with strict production checks
- CSRF: Require CSRF_SECRET in production, throw error on missing secret
- IP Allowlist: TRUST_PROXY must be explicitly set to 'true' for proxy headers
- Rate Limiter: Add proper proxy trust handling for client IP detection
- Login: Add browser form redirect support with safe URL validation
- Add custom admin login page with styled form
- Update CLAUDE.md with TRUST_PROXY documentation
- Update tests for new security behavior

BREAKING: Server will not start in production without CSRF_SECRET or PAYLOAD_SECRET

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 05:06:15 +00:00

196 lines
5.4 KiB
TypeScript

/**
* Custom Login Page
*
* Komplett eigene Login-Seite um den Redirect-Loop-Bug in Payload zu umgehen.
* Diese Seite rendert ein einfaches Login-Formular das direkt mit der Payload API kommuniziert.
*/
import { headers, cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { getPayload } from 'payload'
import configPromise from '@payload-config'
type SearchParams = {
redirect?: string
error?: string
}
export default async function LoginPage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}) {
const resolvedParams = await searchParams
const payload = await getPayload({ config: configPromise })
// Prüfe ob User bereits eingeloggt ist
const headersList = await headers()
const cookieStore = await cookies()
const token = cookieStore.get('payload-token')?.value
if (token) {
try {
const { user } = await payload.auth({ headers: headersList })
if (user) {
// User ist eingeloggt - weiterleiten
const redirectTo = resolvedParams.redirect || '/admin'
// Verhindere Redirect-Loop
if (!redirectTo.includes('/login')) {
redirect(redirectTo)
}
redirect('/admin')
}
} catch {
// Token ungültig - weiter zum Login
}
}
// Bestimme Redirect-Ziel (verhindere Loop)
let redirectTarget = resolvedParams.redirect || '/admin'
if (redirectTarget.includes('/login')) {
redirectTarget = '/admin'
}
const error = resolvedParams.error
return (
<html lang="de">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Anmelden - Payload</title>
<style>{`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #0f0f0f;
color: #fff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: #1a1a1a;
padding: 2rem;
border-radius: 8px;
width: 100%;
max-width: 400px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.logo {
text-align: center;
margin-bottom: 2rem;
}
.logo svg {
width: 48px;
height: 48px;
}
h1 {
font-size: 1.5rem;
font-weight: 600;
text-align: center;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.875rem;
color: #a0a0a0;
}
input {
width: 100%;
padding: 0.75rem;
border: 1px solid #333;
border-radius: 4px;
background: #0f0f0f;
color: #fff;
font-size: 1rem;
}
input:focus {
outline: none;
border-color: #3b82f6;
}
button {
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 4px;
background: #3b82f6;
color: #fff;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
margin-top: 1rem;
}
button:hover {
background: #2563eb;
}
.error {
background: #dc2626;
color: #fff;
padding: 0.75rem;
border-radius: 4px;
margin-bottom: 1rem;
font-size: 0.875rem;
}
`}</style>
</head>
<body>
<div className="login-container">
<div className="logo">
<svg viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.5 3L3 7.5V17.5L11.5 22L20 17.5V7.5L11.5 3Z" stroke="white" strokeWidth="2"/>
<path d="M11.5 12L3 7.5" stroke="white" strokeWidth="2"/>
<path d="M11.5 12V22" stroke="white" strokeWidth="2"/>
<path d="M11.5 12L20 7.5" stroke="white" strokeWidth="2"/>
</svg>
</div>
<h1>Anmelden</h1>
{error && (
<div className="error">
{error === 'invalid' ? 'E-Mail oder Passwort ist falsch.' : error}
</div>
)}
<form action="/api/users/login" method="POST">
<input type="hidden" name="redirect" value={redirectTarget} />
<div className="form-group">
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
name="email"
required
autoComplete="email"
autoFocus
/>
</div>
<div className="form-group">
<label htmlFor="password">Passwort</label>
<input
type="password"
id="password"
name="password"
required
autoComplete="current-password"
/>
</div>
<button type="submit">Anmelden</button>
</form>
</div>
</body>
</html>
)
}