Новые правки в компоенты
This commit is contained in:
parent
e85d1ce668
commit
6f727aae7b
23 changed files with 1483 additions and 37 deletions
109
frontend/src/pages/api/auth/confirm-password-reset.ts
Normal file
109
frontend/src/pages/api/auth/confirm-password-reset.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
|
||||
const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || 'http://localhost:8090';
|
||||
|
||||
const PASSWORD_MIN_LENGTH = 8;
|
||||
const PASSWORD_MAX_LENGTH = 12;
|
||||
const PASSWORD_REGEX = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+$/;
|
||||
|
||||
function validatePassword(password: string): { valid: boolean; error?: string } {
|
||||
if (!password || password.length < PASSWORD_MIN_LENGTH) {
|
||||
return { valid: false, error: 'Пароль должен быть не менее 8 символов' };
|
||||
}
|
||||
if (password.length > PASSWORD_MAX_LENGTH) {
|
||||
return { valid: false, error: 'Пароль не должен превышать 12 символов' };
|
||||
}
|
||||
if (!PASSWORD_REGEX.test(password)) {
|
||||
return { valid: false, error: 'Пароль должен содержать хотя бы одну букву и одну цифру' };
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
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 { token, password, passwordConfirm } = body;
|
||||
|
||||
if (!token) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Токен сброса пароля обязателен' }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
if (!password || !passwordConfirm) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Пароль и подтверждение обязательны' }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Пароли не совпадают' }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const passwordValidation = validatePassword(password);
|
||||
if (!passwordValidation.valid) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: passwordValidation.error }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const confirmUrl = `${POCKETBASE_URL}/api/collections/users/confirm-password-reset`;
|
||||
|
||||
const response = await fetch(confirmUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token, password, passwordConfirm }),
|
||||
});
|
||||
|
||||
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('[CONFIRM_RESET] Error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Внутренняя ошибка сервера'
|
||||
}),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
};
|
||||
73
frontend/src/pages/api/auth/me.ts
Normal file
73
frontend/src/pages/api/auth/me.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import type { APIRoute } from "astro";
|
||||
|
||||
const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || "http://localhost:8090";
|
||||
|
||||
export const GET: APIRoute = async ({ cookies, request }) => {
|
||||
try {
|
||||
const pbAuthCookie = cookies.get("pb_auth")?.value;
|
||||
|
||||
if (!pbAuthCookie) {
|
||||
return new Response(
|
||||
JSON.stringify({ authenticated: false, user: null }),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
const token = pbAuthCookie.trim();
|
||||
|
||||
const response = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/users/auth-refresh`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
if (cookies.has("pb_auth")) {
|
||||
cookies.delete("pb_auth", { path: "/" });
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({ authenticated: false, user: null }),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const isHttps = request.headers.get("x-forwarded-proto") === "https";
|
||||
|
||||
cookies.set("pb_auth", data.token, {
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
httpOnly: false,
|
||||
secure: isHttps,
|
||||
sameSite: "lax",
|
||||
});
|
||||
|
||||
const fullName = [data.record.firstName, data.record.lastName].filter(Boolean).join(" ") || data.record.email;
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
authenticated: true,
|
||||
user: {
|
||||
id: data.record.id,
|
||||
email: data.record.email,
|
||||
name: fullName,
|
||||
avatar: data.record.avatar,
|
||||
verified: data.record.verified,
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[AUTH ME] Error:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ authenticated: false, error: "Ошибка сервера" }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
124
frontend/src/pages/api/auth/request-password-reset.ts
Normal file
124
frontend/src/pages/api/auth/request-password-reset.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
|
||||
const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || 'http://localhost:8090';
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const resetUrl = `${POCKETBASE_URL}/api/collections/users/request-password-reset`;
|
||||
|
||||
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' } }
|
||||
);
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue