Новые правки в компоенты
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' } }
|
||||
);
|
||||
}
|
||||
};
|
||||
248
frontend/src/pages/api/comments/index.ts
Normal file
248
frontend/src/pages/api/comments/index.ts
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import type { APIRoute } from "astro";
|
||||
|
||||
const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || "http://localhost:8090";
|
||||
|
||||
export const GET: APIRoute = async ({ url, cookies }) => {
|
||||
const postSlug = url.searchParams.get("post_slug");
|
||||
const parent = url.searchParams.get("parent");
|
||||
|
||||
if (!postSlug) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "post_slug required" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const pbAuthCookie = cookies.get("pb_auth")?.value;
|
||||
const token = pbAuthCookie?.trim();
|
||||
|
||||
// Фильтр: комментарии для поста + статус published
|
||||
let filter = `post_slug = "${postSlug}" && status = "published"`;
|
||||
|
||||
// Если parent = null — это основные комментарии (не ответы)
|
||||
// Если parent указан — это ответы на конкретный комментарий
|
||||
if (parent === "null" || parent === null) {
|
||||
filter += ` && parent = ""`;
|
||||
} else if (parent) {
|
||||
filter += ` && parent = "${parent}"`;
|
||||
}
|
||||
|
||||
const expand = "user";
|
||||
const sort = "-created";
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/comments/records?expand=${expand}&filter=${encodeURIComponent(filter)}&sort=${sort}`,
|
||||
{
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Comments API GET] Error:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Failed to fetch comments" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const POST: APIRoute = async ({ request, cookies }) => {
|
||||
const pbAuthCookie = cookies.get("pb_auth")?.value;
|
||||
const token = pbAuthCookie?.trim();
|
||||
|
||||
console.log("[Comments API POST] Токен получен:", !!token);
|
||||
console.log("[Comments API POST] Длина токена:", token?.length);
|
||||
console.log("[Comments API POST] Cookie:", pbAuthCookie ? "да" : "нет");
|
||||
|
||||
if (!token) {
|
||||
console.log("[Comments API POST] Токен не найден - 401");
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unauthorized" }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Получаем данные пользователя из токена
|
||||
let userId: string | null = null;
|
||||
try {
|
||||
const userResponse = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/users/auth-refresh`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (userResponse.ok) {
|
||||
const userData = await userResponse.json();
|
||||
userId = userData.record?.id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Comments API POST] Не удалось получить данные пользователя:", e);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
console.log("[Comments API POST] Не удалось получить ID пользователя");
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unauthorized" }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { content, post_slug, parent = null } = body;
|
||||
|
||||
// Валидация
|
||||
if (!content || !post_slug) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "content and post_slug required" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (content.length < 10 || content.length > 2000) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "content must be between 10 and 2000 characters" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Проверка: если это ответ на комментарий
|
||||
if (parent) {
|
||||
const parentCommentResponse = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/comments/records/${parent}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (parentCommentResponse.ok) {
|
||||
const parentComment = await parentCommentResponse.json();
|
||||
|
||||
// Если родительский комментарий уже сам является ответом — запрещаем (только 2 уровня)
|
||||
if (parentComment.parent && parentComment.parent !== "") {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Нельзя отвечать на ответы. Вы можете ответить только на основные комментарии." }),
|
||||
{
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Нельзя отвечать на свой комментарий
|
||||
if (parentComment.user === userId) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Нельзя отвечать на свой собственный коммен<D0B5><D0BD>арий" }),
|
||||
{
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Проверяем: если родительский комментарий уже имеет ответы — запрещаем (только 1 уровень ответов)
|
||||
const existingRepliesResponse = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/comments/records?filter=parent="${parent}"&perPage=1`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (existingRepliesResponse.ok) {
|
||||
const existingRepliesData = await existingRepliesResponse.json();
|
||||
if (existingRepliesData.totalItems > 0) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "На этот комментарий уже есть ответ. Вы можете ответить только на основные комментарии." }),
|
||||
{
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Родительский комментарий не найден" }),
|
||||
{
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/comments/records`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
post_slug,
|
||||
parent: parent || "",
|
||||
status: "published",
|
||||
user: userId, // Передаём ID текущего пользователя
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
console.log("[Comments API POST] Ответ PocketBase:", response.status, JSON.stringify(data));
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: response.status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 201,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Comments API POST] Error:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Failed to create comment" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -39,11 +39,14 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|||
|
||||
const authData = await authResponse.json();
|
||||
|
||||
console.log('[Vote API] Auth refresh full response:', JSON.stringify(authData));
|
||||
|
||||
// ВАЖНО: Явно берем id, а не email
|
||||
const userId = authData.record?.id;
|
||||
const userId = authData.record?.id || authData.record?.ID;
|
||||
const userEmail = authData.record?.email;
|
||||
|
||||
console.log('[Vote API] Auth data:', { userId, userEmail, record: authData.record });
|
||||
console.log('[Vote API] User ID extracted:', userId);
|
||||
console.log('[Vote API] User Email extracted:', userEmail);
|
||||
|
||||
// Защита: проверяем что это действительно ID, а не email
|
||||
if (!userId || EMAIL_REGEX.test(userId)) {
|
||||
|
|
@ -115,9 +118,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|||
}
|
||||
|
||||
// Выполняем операцию с голосом
|
||||
console.log('[Vote API] Creating vote with:', { post: post_id, user: userId, vote_type });
|
||||
|
||||
const voteBody = method === 'POST' ? JSON.stringify({
|
||||
post: post_id,
|
||||
user: userId, // Теперь точно ID, а не email
|
||||
user: userId,
|
||||
vote_type,
|
||||
}) : method === 'PATCH' ? JSON.stringify({ vote_type }) : null;
|
||||
|
||||
|
|
@ -130,6 +135,15 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|||
body: voteBody,
|
||||
});
|
||||
|
||||
console.log('[Vote API] Vote response status:', voteRes.status);
|
||||
if (!voteRes.ok) {
|
||||
const errorText = await voteRes.text();
|
||||
console.log('[Vote API] Vote error response:', errorText);
|
||||
} else {
|
||||
const createdVote = await voteRes.json();
|
||||
console.log('[Vote API] Created vote:', JSON.stringify(createdVote));
|
||||
}
|
||||
|
||||
if (!voteRes.ok && method !== 'DELETE') {
|
||||
const errorText = await voteRes.text();
|
||||
console.error('[Vote API] Failed to save vote:', errorText);
|
||||
|
|
@ -139,16 +153,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|||
if (method === 'POST') userVote = vote_type;
|
||||
if (method === 'DELETE') userVote = null;
|
||||
|
||||
// Получаем актуальные счетчики
|
||||
// Получаем актуальные счетчики (без авторизации)
|
||||
const votesRes = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/post_votes/records?` +
|
||||
new URLSearchParams({
|
||||
filter: `post="${post_id}"`,
|
||||
fields: 'vote_type',
|
||||
}),
|
||||
{
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let likes = 0;
|
||||
|
|
@ -160,7 +171,8 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|||
dislikes = votesData.items.filter((v: any) => v.vote_type === 'dislike').length;
|
||||
}
|
||||
|
||||
// Обновляем счетчики в посте
|
||||
// Обновляем счетчики в посте только если есть токен
|
||||
if (token) {
|
||||
await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/posts/records/${post_id}`,
|
||||
{
|
||||
|
|
@ -172,6 +184,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|||
body: JSON.stringify({ likes, dislikes }),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ likes, dislikes, userVote }),
|
||||
|
|
@ -216,6 +229,24 @@ export const GET: APIRoute = async ({ url, cookies }) => {
|
|||
|
||||
const post = await postRes.json();
|
||||
|
||||
// Получаем счетчики из коллекции голосов (без авторизации)
|
||||
const votesCountRes = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/post_votes/records?` +
|
||||
new URLSearchParams({
|
||||
filter: `post="${postId}"`,
|
||||
fields: 'vote_type',
|
||||
})
|
||||
);
|
||||
|
||||
let likes = post.likes || 0;
|
||||
let dislikes = post.dislikes || 0;
|
||||
|
||||
if (votesCountRes.ok) {
|
||||
const votesData = await votesCountRes.json();
|
||||
likes = votesData.items.filter((v: any) => v.vote_type === 'like').length;
|
||||
dislikes = votesData.items.filter((v: any) => v.vote_type === 'dislike').length;
|
||||
}
|
||||
|
||||
// Определяем голос текущего пользователя
|
||||
let userVote: 'like' | 'dislike' | null = null;
|
||||
|
||||
|
|
@ -232,9 +263,12 @@ export const GET: APIRoute = async ({ url, cookies }) => {
|
|||
|
||||
if (authRes.ok) {
|
||||
const authData = await authRes.json();
|
||||
const userId = authData.record?.id;
|
||||
console.log('[Vote API] GET auth refresh:', JSON.stringify(authData.record));
|
||||
const userId = authData.record?.id || authData.record?.ID;
|
||||
|
||||
if (userId && POCKETBASE_ID_REGEX.test(userId)) {
|
||||
console.log('[Vote API] GET: Checking for existing vote with:', { post: postId, user: userId });
|
||||
|
||||
const userVoteRes = await fetch(
|
||||
`${POCKETBASE_URL}/api/collections/post_votes/records?` +
|
||||
new URLSearchParams({
|
||||
|
|
@ -260,8 +294,8 @@ export const GET: APIRoute = async ({ url, cookies }) => {
|
|||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
likes: post.likes || 0,
|
||||
dislikes: post.dislikes || 0,
|
||||
likes,
|
||||
dislikes,
|
||||
userVote,
|
||||
}),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' }}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ import { SITE_URL } from '@constants';
|
|||
submitBtn.textContent = 'Отправка...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/forgot-password', {
|
||||
const response = await fetch('/api/auth/request-password-reset', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
|
|
|
|||
|
|
@ -157,10 +157,10 @@ const error = Astro.url.searchParams.get('error');
|
|||
submitBtn.textContent = 'Сохранение...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/reset-password', {
|
||||
const response = await fetch('/api/auth/confirm-password-reset', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token, userId, password }),
|
||||
body: JSON.stringify({ token, password, passwordConfirm }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
import ArticleLayout from '@layouts/ArticleLayout.astro';
|
||||
import { SITE_URL } from '@constants';
|
||||
import PostCommentForm from '@components/blog/PostCommentForm.astro';
|
||||
import Comments from '@components/blog/comments/Comments';
|
||||
import RelatedPosts from '@components/blog/RelatedPosts.astro';
|
||||
import ArticleTableOfContents from '@components/blog/ArticleTableOfContents.astro';
|
||||
import { getPostBySlug, getPosts, getPostImageUrl, getPostVotesStats } from '@lib/pb';
|
||||
|
|
@ -71,6 +71,7 @@ const heroImage = getPostImageUrl(post);
|
|||
date={formatDate(post.date)}
|
||||
author={post.author}
|
||||
readTime={post.readTime}
|
||||
readmeTime={post.readmeTime}
|
||||
postId={post.id}
|
||||
postUrl={currentUrl}
|
||||
initialLikes={likes}
|
||||
|
|
@ -79,11 +80,10 @@ const heroImage = getPostImageUrl(post);
|
|||
<!-- Содержимое статьи -->
|
||||
<div class="post-content" set:html={contentHtml} />
|
||||
|
||||
<!-- Форма комментариев -->
|
||||
<PostCommentForm
|
||||
postId={post.id}
|
||||
isAuthorized={false}
|
||||
/>
|
||||
<!-- Система комментариев -->
|
||||
<div class="comments-wrapper">
|
||||
<Comments client:only="solid-js" postSlug={post.slug} />
|
||||
</div>
|
||||
|
||||
<!-- Похожие статьи -->
|
||||
<RelatedPosts
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue