import { useValidatedParams, zh } from 'h3-zod'; import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import prisma from '~~/lib/prisma'; import { minioClient, s3Bucket, s3Host } from '~~/lib/s3'; export default defineEventHandler(async (event) => { const { questId } = await useValidatedParams( event, z.object({ questId: zh.numAsString }) ); const user = await requireUserSession(event); const userId = user.user.id; const quest = await prisma.huntQuest.findUnique({ where: { id: questId }, select: { id: true, hunt: { select: { teams: { where: { OR: [ { ownerId: userId }, { members: { some: { memberId: userId } } } ] } } } }, answers: { select: { id: true, text: true, picture: true, updatedAt: true }, where: { team: { OR: [ { ownerId: userId }, { members: { some: { memberId: userId } } } ] } } } } }); if (!quest) { throw createError({ status: 404, statusText: 'Not found!' }); } if (quest.hunt.teams.length <= 0) { throw createError({ status: 403, statusText: 'No team yet!' }); } const team = quest.hunt.teams[0]; const body = await readFormData(event); const formDataObj = Object.fromEntries(body.entries()); const data = z .object({ updatedAt: z.iso.datetime().optional(), answer: z.string().optional(), image: z .file() .mime(['image/png', 'image/jpeg', 'image/jpg']) .max(10_000_000) .optional() }) .parse(formDataObj); const prevAnswer = quest.answers.length > 0 ? quest.answers[0] : { updatedAt: null, id: null }; if ( prevAnswer.updatedAt && (!data.updatedAt || new Date(data.updatedAt).getTime() < new Date(prevAnswer.updatedAt).getTime()) ) { throw createError({ status: 409, statusText: 'Updating on old data, please refresh!' }); } // TODO: prevent updating reviewed answers // TODO: respect end of hunt let picture: { creatorId: number; url: string; uuid: string } | undefined = undefined; if (data.image) { const uuid = randomUUID(); await minioClient.putObject( s3Bucket, uuid, Buffer.from(await data.image.arrayBuffer()), undefined, { 'Cache-Control': 'public, max-age=31536000, immutable', 'Content-Type': data.image.type } ); picture = { creatorId: userId, url: `https://${s3Bucket}.${s3Host}/${uuid}`, uuid }; } if (picture) { await prisma.$transaction(async (tx) => { if (prevAnswer.id) { const file = await tx.file.create({ data: { ...picture, answerId: prevAnswer.id }, select: { id: true } }); await tx.questAnswer.update({ where: { id: prevAnswer.id }, data: { memberId: userId, text: data.answer, pictureId: file.id } }); } else { const answer = await tx.questAnswer.create({ data: { teamId: team.id, memberId: userId, questId: quest.id, text: data.answer }, select: { id: true } }); await tx.file.create({ data: { ...picture, answerId: answer.id, answers: { connect: answer } } }); } }); } else { if (prevAnswer.id) { await prisma.questAnswer.update({ where: { id: prevAnswer.id }, data: { memberId: userId, text: data.answer } }); } else { await prisma.questAnswer.create({ data: { teamId: team.id, memberId: userId, questId: quest.id, text: data.answer } }); } } return { ok: true }; });