140 lines
5.6 KiB
JavaScript
140 lines
5.6 KiB
JavaScript
import { prisma } from '../../lib/prisma.js'
|
|
import { publicReviewAuthorDisplay } from '../../lib/review-display.js'
|
|
import { persistMultipartImages } from '../../lib/upload-images.js'
|
|
import {
|
|
formatFileTooLargeMessage,
|
|
getOtherUploadMaxFileBytes,
|
|
isMultipartFileTooLargeError,
|
|
} from '../../lib/upload-limits.js'
|
|
|
|
export async function registerPublicReviewRoutes(fastify) {
|
|
fastify.post('/api/reviews/upload-image', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
|
try {
|
|
const urls = await persistMultipartImages(request, {
|
|
maxFiles: 1,
|
|
maxFileBytes: getOtherUploadMaxFileBytes(),
|
|
subdir: 'reviews',
|
|
})
|
|
if (urls.length !== 1) return reply.code(400).send({ error: 'Нужно прикрепить 1 изображение' })
|
|
return { url: urls[0] }
|
|
} catch (error) {
|
|
let message = error instanceof Error ? error.message : 'Не удалось загрузить изображение'
|
|
let statusCode =
|
|
error && typeof error === 'object' && 'statusCode' in error && Number.isInteger(error.statusCode)
|
|
? Number(error.statusCode)
|
|
: 400
|
|
if (isMultipartFileTooLargeError(error)) {
|
|
message = formatFileTooLargeMessage(getOtherUploadMaxFileBytes())
|
|
statusCode = 413
|
|
}
|
|
return reply.code(statusCode).send({ error: message })
|
|
}
|
|
})
|
|
|
|
fastify.get('/api/reviews/latest', async (request, reply) => {
|
|
const limitRaw = request.query?.limit
|
|
const limitParsed = typeof limitRaw === 'string' ? Number(limitRaw) : Number(limitRaw)
|
|
const parsed = Number.isFinite(limitParsed) && limitParsed > 0 ? Math.floor(limitParsed) : 5
|
|
const take = Math.min(parsed, 5)
|
|
|
|
const rows = await prisma.review.findMany({
|
|
where: { status: 'approved', product: { published: true } },
|
|
include: {
|
|
user: { select: { email: true, displayName: true } },
|
|
product: { select: { id: true, title: true } },
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
take,
|
|
})
|
|
|
|
const items = rows.map((r) => ({
|
|
id: r.id,
|
|
rating: r.rating,
|
|
text: r.text,
|
|
imageUrl: r.imageUrl,
|
|
createdAt: r.createdAt,
|
|
authorDisplay: publicReviewAuthorDisplay(r.user),
|
|
productId: r.productId,
|
|
productTitle: r.product?.title ?? '',
|
|
}))
|
|
|
|
return { items }
|
|
})
|
|
|
|
fastify.get('/api/products/:id/reviews', async (request, reply) => {
|
|
const { id } = request.params
|
|
|
|
const pageRaw = request.query?.page
|
|
const pageParsed = typeof pageRaw === 'string' ? Number(pageRaw) : Number(pageRaw)
|
|
const page = Number.isFinite(pageParsed) && pageParsed > 0 ? Math.floor(pageParsed) : 1
|
|
|
|
const pageSizeRaw = request.query?.pageSize
|
|
const pageSizeParsed = typeof pageSizeRaw === 'string' ? Number(pageSizeRaw) : Number(pageSizeRaw)
|
|
const pageSize = Number.isFinite(pageSizeParsed) && pageSizeParsed > 0 ? Math.floor(pageSizeParsed) : 10
|
|
if (pageSize > 50) return reply.code(400).send({ error: 'pageSize должен быть ≤ 50' })
|
|
|
|
const product = await prisma.product.findFirst({ where: { id, published: true } })
|
|
if (!product) return reply.code(404).send({ error: 'Товар не найден' })
|
|
|
|
const where = { productId: id, status: 'approved' }
|
|
const total = await prisma.review.count({ where })
|
|
const rawItems = await prisma.review.findMany({
|
|
where,
|
|
include: { user: { select: { email: true, displayName: true } } },
|
|
orderBy: { createdAt: 'desc' },
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
})
|
|
|
|
const items = rawItems.map((r) => ({
|
|
id: r.id,
|
|
rating: r.rating,
|
|
text: r.text,
|
|
imageUrl: r.imageUrl,
|
|
createdAt: r.createdAt,
|
|
authorDisplay: publicReviewAuthorDisplay(r.user),
|
|
}))
|
|
|
|
return { items, total, page, pageSize }
|
|
})
|
|
|
|
fastify.post('/api/products/:id/reviews', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
|
const userId = request.user.sub
|
|
const { id: productId } = request.params
|
|
|
|
const product = await prisma.product.findFirst({ where: { id: productId, published: true } })
|
|
if (!product) return reply.code(404).send({ error: 'Товар не найден' })
|
|
|
|
const rating = Number(request.body?.rating)
|
|
if (!Number.isFinite(rating) || rating < 1 || rating > 5) {
|
|
return reply.code(400).send({ error: 'rating должен быть от 1 до 5' })
|
|
}
|
|
const textRaw = request.body?.text
|
|
const text = textRaw === null || textRaw === undefined ? null : String(textRaw).trim()
|
|
if (text !== null && text.length > 1000) return reply.code(400).send({ error: 'Отзыв слишком длинный' })
|
|
const imageUrlRaw = request.body?.imageUrl
|
|
const imageUrl = imageUrlRaw === null || imageUrlRaw === undefined ? null : String(imageUrlRaw).trim()
|
|
if (imageUrl !== null && imageUrl.length > 300)
|
|
return reply.code(400).send({ error: 'Ссылка на фото слишком длинная' })
|
|
if (imageUrl !== null && imageUrl.length > 0 && !imageUrl.startsWith('/uploads/')) {
|
|
return reply.code(400).send({ error: 'Некорректная ссылка на изображение' })
|
|
}
|
|
|
|
try {
|
|
const created = await prisma.review.create({
|
|
data: {
|
|
productId,
|
|
userId,
|
|
rating: Math.floor(rating),
|
|
text: text && text.length ? text : null,
|
|
imageUrl: imageUrl && imageUrl.length ? imageUrl : null,
|
|
status: 'pending',
|
|
},
|
|
})
|
|
return reply.code(201).send({ item: created })
|
|
} catch {
|
|
return reply.code(409).send({ error: 'Вы уже оставляли отзыв на этот товар' })
|
|
}
|
|
})
|
|
}
|