import type { APIRoute } from 'astro'; import { pb } from '../../../lib/pb'; import { sendEmail, getSiteUrl } from '../../../lib/email'; const RATE_LIMIT_MAX_REQUESTS = 3; const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; const rateLimitStore = new Map(); function cleanOldRecords(email: string, now: number) { const timestamps = rateLimitStore.get(email) || []; const windowStart = now - RATE_LIMIT_WINDOW_MS; const validTimestamps = timestamps.filter(ts => ts > windowStart); if (validTimestamps.length === 0) { rateLimitStore.delete(email); } else { rateLimitStore.set(email, validTimestamps); } } function checkRateLimit(email: string): { allowed: boolean; remaining?: number; resetIn?: number } { const now = Date.now(); const normalizedEmail = email.toLowerCase().trim(); cleanOldRecords(normalizedEmail, now); const timestamps = rateLimitStore.get(normalizedEmail) || []; if (timestamps.length >= RATE_LIMIT_MAX_REQUESTS) { const oldestTimestamp = timestamps[0]; const resetIn = RATE_LIMIT_WINDOW_MS - (now - oldestTimestamp); return { allowed: false, resetIn }; } timestamps.push(now); rateLimitStore.set(normalizedEmail, timestamps); return { allowed: true, remaining: RATE_LIMIT_MAX_REQUESTS - timestamps.length }; } function generateResetPasswordHtml(firstName: string, resetLink: string): string { return ` Сброс пароля

Автоюрист Сургут

Юридические услуги для автовладельцев

Сброс пароля

Здравствуйте, ${firstName}!

Вы запросили сброс пароля. Нажмите кнопку ниже для создания нового пароля:

Сбросить пароль

Ссылка действительна 1 час. Если вы не запрашивали сброс пароля, просто проигнорируйте это письмо.

© 2026 Автоюрист Сургут. Все права защищены.

`; } export const POST: APIRoute = async ({ request }) => { try { let body; try { body = await request.json(); } catch { return new Response( JSON.stringify({ error: 'Неверный формат данных' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } const { email } = body; if (!email) { return new Response( JSON.stringify({ error: 'Email обязателен' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } const rateLimitResult = checkRateLimit(email); if (!rateLimitResult.allowed) { const resetMinutes = Math.ceil((rateLimitResult.resetIn || 0) / 60000); return new Response( JSON.stringify({ error: 'Слишком много запросов. Попробуйте позже', retryAfter: resetMinutes }), { status: 429, headers: { 'Content-Type': 'application/json', 'Retry-After': String(resetMinutes * 60) } } ); } // Проверяем существует ли пользователь let user = null; try { user = await pb.collection('users').getFirstListItem(`email="${email}"`); } catch (e) { console.log('User not found, still return success'); } if (!user) { return new Response( JSON.stringify({ success: true, message: 'Ссылка для сброса пароля отправлена' }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); } // Создаём свой токен сброса const resetToken = Buffer.from(`${user.id}:${Date.now()}`).toString('base64').replace(/=/g, ''); const resetLink = `${getSiteUrl()}/auth/reset-password?token=${resetToken}&userId=${user.id}`; // Отправляем письмо через SMTP.BZ const firstName = user.firstName || 'Пользователь'; const html = generateResetPasswordHtml(firstName, resetLink); const emailSent = await sendEmail({ to: email, subject: 'Сброс пароля — Автоюрист Сургут', html }); console.log('Password reset email sent:', emailSent); if (!emailSent) { return new Response( JSON.stringify({ error: 'Не удалось отправить письмо. Попробуйте позже.' }), { status: 500, headers: { 'Content-Type': 'application/json' } } ); } return new Response( JSON.stringify({ success: true, message: 'Письмо для сброса пароля отправлено' }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); } catch (error) { console.error('[PASSWORD_RESET] Error:', error); return new Response( JSON.stringify({ error: 'Внутренняя ошибка сервера' }), { status: 500, headers: { 'Content-Type': 'application/json' } } ); } };