2026-04-18 18:25:10 +05:00
|
|
|
|
import type { APIRoute } from 'astro';
|
|
|
|
|
|
|
2026-05-04 03:10:06 +05:00
|
|
|
|
const PB_POCKETBASE_URL = import.meta.env.PB_POCKETBASE_URL || 'http://localhost:8090';
|
2026-04-18 18:25:10 +05:00
|
|
|
|
|
|
|
|
|
|
const RATE_LIMIT_MAX_REQUESTS = 3;
|
|
|
|
|
|
const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
const rateLimitStore = new Map<string, number[]>();
|
|
|
|
|
|
|
|
|
|
|
|
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 };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 03:10:06 +05:00
|
|
|
|
const resetUrl = `${PB_POCKETBASE_URL}/api/collections/users/request-password-reset`;
|
2026-04-18 18:25:10 +05:00
|
|
|
|
|
|
|
|
|
|
const response = await fetch(resetUrl, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({ email }),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (response.status === 204) {
|
|
|
|
|
|
return new Response(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: 'Письмо для сброса пароля отправлено'
|
|
|
|
|
|
}),
|
|
|
|
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let data;
|
|
|
|
|
|
try {
|
|
|
|
|
|
data = await response.json();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return new Response(
|
|
|
|
|
|
JSON.stringify({ error: 'Ошибка обработки ответа' }),
|
|
|
|
|
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new Response(
|
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
|
error: data.message || 'Ошибка при отправке письма для сброса пароля'
|
|
|
|
|
|
}),
|
|
|
|
|
|
{ status: response.status, 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' } }
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|