base commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "OrderMessage" ADD COLUMN "attachmentUrl" TEXT;
|
||||
@@ -147,11 +147,13 @@ model OrderItem {
|
||||
}
|
||||
|
||||
model OrderMessage {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
/// 'user' | 'admin'
|
||||
authorType String
|
||||
text String
|
||||
createdAt DateTime @default(now())
|
||||
authorType String
|
||||
text String
|
||||
/// URL вида /uploads/… (чек к оплате и т.п.)
|
||||
attachmentUrl String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
orderId String
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/** Минимальное экранирование для безопасного HTML из пользовательского ввода. */
|
||||
export function escapeHtml(input) {
|
||||
return String(input ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
@@ -42,3 +42,19 @@ export async function persistMultipartImages(request, { maxFiles = 10 } = {}) {
|
||||
|
||||
return urls
|
||||
}
|
||||
|
||||
/** Сохранить один буфер изображения в uploads/, вернуть путь `/uploads/...`. */
|
||||
export async function saveImageBufferToUploads(originalFilename, buffer) {
|
||||
const ext = safeImageExt(originalFilename)
|
||||
if (!ext) {
|
||||
throw uploadError('Разрешены только файлы: png, jpg, jpeg, webp')
|
||||
}
|
||||
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads')
|
||||
await fs.promises.mkdir(uploadsDir, { recursive: true })
|
||||
|
||||
const fileName = `${crypto.randomUUID()}${ext}`
|
||||
const fullPath = path.join(uploadsDir, fileName)
|
||||
await fs.promises.writeFile(fullPath, buffer)
|
||||
return `/uploads/${fileName}`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { prisma } from '../lib/prisma.js'
|
||||
import { issueEmailCode, normalizeEmail, verifyEmailCode } from '../lib/auth.js'
|
||||
import { escapeHtml } from '../lib/escape-html.js'
|
||||
import { prisma } from '../lib/prisma.js'
|
||||
import { saveImageBufferToUploads } from '../lib/upload-images.js'
|
||||
|
||||
function mapUserForClient(user) {
|
||||
const adminEmail = normalizeEmail(process.env.ADMIN_EMAIL)
|
||||
@@ -704,11 +706,96 @@ export async function registerAuthRoutes(fastify) {
|
||||
if (order.status === 'DRAFT') {
|
||||
await prisma.order.update({ where: { id }, data: { status: 'PENDING_PAYMENT' } })
|
||||
nextStatus = 'PENDING_PAYMENT'
|
||||
} else if (order.status === 'PENDING_PAYMENT') {
|
||||
await prisma.order.update({ where: { id }, data: { status: 'PAYMENT_VERIFICATION' } })
|
||||
nextStatus = 'PAYMENT_VERIFICATION'
|
||||
return { ok: true, status: nextStatus }
|
||||
}
|
||||
return { ok: true, status: nextStatus }
|
||||
|
||||
if (order.status === 'PAYMENT_VERIFICATION') {
|
||||
return { ok: true, status: nextStatus }
|
||||
}
|
||||
|
||||
if (order.status === 'PENDING_PAYMENT') {
|
||||
if (!request.isMultipart()) {
|
||||
return reply
|
||||
.code(400)
|
||||
.send({ error: 'Отправьте multipart/form-data: поле detail и/или файл receipt' })
|
||||
}
|
||||
|
||||
let detail = ''
|
||||
let receiptBuffer = null
|
||||
let receiptFilename = ''
|
||||
try {
|
||||
const parts = request.parts()
|
||||
for await (const part of parts) {
|
||||
if (part.type === 'file') {
|
||||
if (part.fieldname === 'receipt') {
|
||||
if (receiptBuffer !== null) {
|
||||
return reply.code(400).send({ error: 'Допускается один файл receipt' })
|
||||
}
|
||||
receiptBuffer = await part.toBuffer()
|
||||
receiptFilename = part.filename ?? 'receipt'
|
||||
}
|
||||
} else if (part.type === 'field' && part.fieldname === 'detail') {
|
||||
detail = String(part.value ?? '').trim()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Не удалось разобрать форму'
|
||||
return reply.code(400).send({ error: msg })
|
||||
}
|
||||
|
||||
const hasDetail = detail.length > 0
|
||||
const hasReceipt = receiptBuffer !== null && receiptBuffer.length > 0
|
||||
|
||||
if (!hasDetail && !hasReceipt) {
|
||||
return reply
|
||||
.code(400)
|
||||
.send({ error: 'Укажите текст о платеже и/или прикрепите изображение чека' })
|
||||
}
|
||||
|
||||
const maxDetail = 2000
|
||||
if (detail.length > maxDetail) {
|
||||
return reply.code(400).send({ error: `Текст не длиннее ${maxDetail} символов` })
|
||||
}
|
||||
|
||||
let attachmentUrl = null
|
||||
if (hasReceipt) {
|
||||
try {
|
||||
attachmentUrl = await saveImageBufferToUploads(receiptFilename, receiptBuffer)
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Не удалось сохранить файл'
|
||||
const statusCode =
|
||||
err && typeof err === 'object' && 'statusCode' in err && Number.isInteger(err.statusCode)
|
||||
? Number(err.statusCode)
|
||||
: 400
|
||||
return reply.code(statusCode).send({ error: message })
|
||||
}
|
||||
}
|
||||
|
||||
const bodyHtml = hasDetail
|
||||
? `<p>${escapeHtml(detail).replace(/\r\n|\n|\r/g, '<br/>')}</p>`
|
||||
: ''
|
||||
const messageText = `<p><strong>Подтверждение оплаты (перевод ВТБ / Сбербанк)</strong></p>${bodyHtml}`
|
||||
|
||||
try {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.order.update({ where: { id }, data: { status: 'PAYMENT_VERIFICATION' } })
|
||||
await tx.orderMessage.create({
|
||||
data: {
|
||||
orderId: id,
|
||||
authorType: 'user',
|
||||
text: messageText,
|
||||
attachmentUrl,
|
||||
},
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
return reply.code(500).send({ error: 'Не удалось сохранить оплату' })
|
||||
}
|
||||
|
||||
return { ok: true, status: 'PAYMENT_VERIFICATION' }
|
||||
}
|
||||
|
||||
return reply.code(409).send({ error: 'Сейчас нельзя выполнить оплату для этого заказа' })
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user