initial: server + shared

This commit is contained in:
admin
2026-06-11 13:41:38 +05:00
commit 65da047e7c
148 changed files with 15900 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { asyncHandler } from '../../lib/async-handler.js'
import { prisma } from '../../lib/prisma.js'
import { persistMultipartImages } from '../../lib/upload-images.js'
import {
formatFileTooLargeMessage,
getProductImageMaxFileBytes,
isMultipartFileTooLargeError,
} from '../../lib/upload-limits.js'
export async function registerAdminGalleryRoutes(fastify) {
fastify.get('/api/admin/gallery', { preHandler: [fastify.verifyAdmin] }, async () => {
const items = await prisma.galleryImage.findMany({
orderBy: { createdAt: 'desc' },
})
const urls = items.map((i) => i.url)
const usedUrls = new Set()
const productImages = await prisma.productImage.findMany({
where: { url: { in: urls } },
select: { url: true },
})
for (const pi of productImages) {
usedUrls.add(pi.url)
}
const legacyProducts = await prisma.product.findMany({
where: { imageUrl: { in: urls } },
select: { imageUrl: true },
})
for (const p of legacyProducts) {
if (p.imageUrl) usedUrls.add(p.imageUrl)
}
return {
items: items.map((i) => ({
id: i.id,
url: i.url,
isResized: i.isResized,
createdAt: i.createdAt,
inUse: usedUrls.has(i.url),
})),
}
})
fastify.post('/api/admin/gallery/upload', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
try {
const urls = await persistMultipartImages(request, {
maxFiles: 10,
maxFileBytes: getProductImageMaxFileBytes(),
subdir: '',
eager: false,
})
for (const url of urls) {
await prisma.galleryImage.create({
data: { url, isResized: false },
})
}
return { urls }
} 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(getProductImageMaxFileBytes())
statusCode = 413
}
return reply.code(statusCode).send({ error: message })
}
})
fastify.post(
'/api/admin/gallery/:id/resize',
{ preHandler: [fastify.verifyAdmin] },
asyncHandler(async (request, reply) => {
const { id } = request.params
const row = await prisma.galleryImage.findUnique({ where: { id } })
if (!row) {
return reply.code(404).send({ error: 'Изображение не найдено' })
}
if (row.isResized) {
return reply.code(409).send({ error: 'Изображение уже обработано' })
}
const urlParts = row.url.replace(/^\//, '').split('/')
const fileName = urlParts[urlParts.length - 1]
const uuid = path.parse(fileName).name
const { generateAllSizes, convertOriginalToWebp } = await import('../../lib/image-resize.js')
const fullPath = path.join(process.cwd(), urlParts.slice(0, -1).join('/'), fileName)
await generateAllSizes(uuid, '', fullPath)
const newUrl = await convertOriginalToWebp(uuid, '')
await prisma.galleryImage.update({
where: { id },
data: { url: newUrl, isResized: true },
})
return { url: newUrl }
}),
)
fastify.delete('/api/admin/gallery/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
const { id } = request.params
const row = await prisma.galleryImage.findUnique({ where: { id } })
if (!row) {
return reply.code(404).send({ error: 'Не найдено' })
}
const usedInImages = await prisma.productImage.count({ where: { url: row.url } })
const usedAsLegacy = await prisma.product.count({ where: { imageUrl: row.url } })
if (usedInImages > 0 || usedAsLegacy > 0) {
return reply.code(409).send({ error: 'Изображение используется в карточке товара' })
}
const relative = row.url.replace(/^\//, '')
const filePath = path.join(process.cwd(), relative)
try {
await fs.unlink(filePath)
} catch (err) {
if (err && typeof err === 'object' && 'code' in err && err.code !== 'ENOENT') {
throw err
}
}
await prisma.galleryImage.delete({ where: { id } })
return reply.code(204).send()
})
}