- Add security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.) - Strengthen password requirements (10+ chars, mixed case, numbers) - Increase shared list slug entropy from 4 to 16 bytes - Add rate limiting to login, registration, upload, and restore endpoints - Add file magic number validation for image uploads (JPEG, PNG, WebP, HEIC) - Add CSV row limit (50k) to restore endpoint - Update client-side registration form to match new password policy Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
81 lines
2.3 KiB
TypeScript
81 lines
2.3 KiB
TypeScript
import { NextResponse } from "next/server"
|
|
import { z } from "zod"
|
|
import bcrypt from "bcryptjs"
|
|
import { prisma } from "@/lib/prisma"
|
|
import { rateLimit } from "@/lib/rate-limit"
|
|
|
|
const registerSchema = z.object({
|
|
name: z
|
|
.string()
|
|
.min(1, "Name is required")
|
|
.max(100, "Name must be 100 characters or less"),
|
|
email: z
|
|
.string()
|
|
.min(1, "Email is required")
|
|
.email("Invalid email address"),
|
|
password: z
|
|
.string()
|
|
.min(10, "Password must be at least 10 characters")
|
|
.max(128, "Password must be 128 characters or less")
|
|
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
|
|
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
|
|
.regex(/[0-9]/, "Password must contain at least one number"),
|
|
})
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
// Rate limit: 5 registration attempts per IP per minute
|
|
const forwarded = request.headers.get("x-forwarded-for")
|
|
const ip = forwarded?.split(",")[0]?.trim() ?? "unknown"
|
|
const rl = rateLimit(`register:${ip}`, 5, 60 * 1000)
|
|
if (!rl.success) {
|
|
return NextResponse.json(
|
|
{ error: "Too many registration attempts. Please try again later." },
|
|
{ status: 429 }
|
|
)
|
|
}
|
|
|
|
const body = await request.json()
|
|
const result = registerSchema.safeParse(body)
|
|
|
|
if (!result.success) {
|
|
const errors = result.error.flatten().fieldErrors
|
|
return NextResponse.json(
|
|
{ error: "Validation failed", details: errors },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
const { name, email, password } = result.data
|
|
|
|
const existingUser = await prisma.user.findUnique({ where: { email } })
|
|
if (existingUser) {
|
|
return NextResponse.json(
|
|
{ error: "An account with this email already exists" },
|
|
{ status: 409 }
|
|
)
|
|
}
|
|
|
|
const hashedPassword = await bcrypt.hash(password, 10)
|
|
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
name,
|
|
email,
|
|
password: hashedPassword,
|
|
emailVerified: new Date(),
|
|
},
|
|
})
|
|
|
|
return NextResponse.json(
|
|
{ id: user.id, name: user.name, email: user.email },
|
|
{ status: 201 }
|
|
)
|
|
} catch {
|
|
return NextResponse.json(
|
|
{ error: "Something went wrong. Please try again." },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|