export const runtime = "nodejs"; import { NextResponse } from "next/server"; import { z } from "zod"; import { db } from "@/lib/db/client"; import { verifyPassword } from "@/lib/auth/password"; import { createSession, setSessionCookie } from "@/lib/auth/session"; const loginSchema = z.object({ email: z.string().email(), password: z.string().min(1), }); // Simple in-memory rate limit — good enough at MVP scale, resets on restart. const attempts = new Map(); const MAX_ATTEMPTS = 10; const WINDOW_MS = 15 * 60 * 1000; function isRateLimited(key: string): boolean { const now = Date.now(); const entry = attempts.get(key); if (!entry || entry.resetAt < now) { attempts.set(key, { count: 1, resetAt: now + WINDOW_MS }); return false; } entry.count += 1; return entry.count > MAX_ATTEMPTS; } export async function POST(request: Request) { const body = await request.json().catch(() => null); const parsed = loginSchema.safeParse(body); if (!parsed.success) { return NextResponse.json({ error: "Некорректные данные" }, { status: 400 }); } const email = parsed.data.email.toLowerCase().trim(); const ip = request.headers.get("x-forwarded-for") ?? "unknown"; if (isRateLimited(`${ip}:${email}`)) { return NextResponse.json({ error: "Слишком много попыток — попробуйте позже" }, { status: 429 }); } const user = await db.query.users.findFirst({ where: (u, { eq }) => eq(u.email, email), }); // Always run verifyPassword (even against a placeholder hash) so the // response timing doesn't reveal whether the email exists. const ok = await verifyPassword( user?.passwordHash ?? "$argon2id$v=19$m=65536,t=3,p=4$00000000000000000000000000$0000000000000000000000000000000000000000000000000000000000000000", parsed.data.password, ); if (!user || !ok) { return NextResponse.json({ error: "Неверный email или пароль" }, { status: 401 }); } const token = await createSession(user.id); await setSessionCookie(token); return NextResponse.json({ ok: true, user: { id: user.id, name: user.name, role: user.role } }); }