From e491becbc4547d9098d87c3efdf7c462646a72b9 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Mon, 6 Oct 2025 23:41:03 -0300 Subject: [PATCH] Fix attachment previews and comment permissions --- convex/tickets.ts | 28 ++++++++++++++--- src/app/tickets/[id]/page.tsx | 6 ++-- .../tickets/new-ticket-dialog.client.tsx | 30 +++++++++++++++++++ src/components/ui/dropzone.tsx | 9 +++--- 4 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 src/components/tickets/new-ticket-dialog.client.tsx diff --git a/convex/tickets.ts b/convex/tickets.ts index dfda7aa..2b30b81 100644 --- a/convex/tickets.ts +++ b/convex/tickets.ts @@ -723,6 +723,10 @@ export const updateComment = mutation({ throw new ConvexError("Ticket não encontrado") } const ticketDoc = ticket as Doc<"tickets"> + const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null + if (!actor || actor.tenantId !== ticketDoc.tenantId) { + throw new ConvexError("Autor do comentário inválido") + } const comment = await ctx.db.get(commentId); if (!comment || comment.ticketId !== ticketId) { throw new ConvexError("Comentário não encontrado"); @@ -730,8 +734,15 @@ export const updateComment = mutation({ if (comment.authorId !== actorId) { throw new ConvexError("Você não tem permissão para editar este comentário"); } + const normalizedRole = (actor.role ?? "AGENT").toUpperCase() if (ticketDoc.requesterId === actorId) { - await requireCustomer(ctx, actorId, ticketDoc.tenantId) + if (normalizedRole === "CUSTOMER") { + await requireCustomer(ctx, actorId, ticketDoc.tenantId) + } else if (STAFF_ROLES.has(normalizedRole)) { + await requireTicketStaff(ctx, actorId, ticketDoc) + } else { + throw new ConvexError("Autor não possui permissão para editar") + } } else { await requireTicketStaff(ctx, actorId, ticketDoc) } @@ -742,7 +753,6 @@ export const updateComment = mutation({ updatedAt: now, }); - const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null; await ctx.db.insert("ticketEvents", { ticketId, type: "COMMENT_EDITED", @@ -772,6 +782,10 @@ export const removeCommentAttachment = mutation({ throw new ConvexError("Ticket não encontrado") } const ticketDoc = ticket as Doc<"tickets"> + const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null + if (!actor || actor.tenantId !== ticketDoc.tenantId) { + throw new ConvexError("Autor do comentário inválido") + } const comment = await ctx.db.get(commentId); if (!comment || comment.ticketId !== ticketId) { throw new ConvexError("Comentário não encontrado"); @@ -780,8 +794,15 @@ export const removeCommentAttachment = mutation({ throw new ConvexError("Você não pode alterar anexos de outro usuário") } + const normalizedRole = (actor.role ?? "AGENT").toUpperCase() if (ticketDoc.requesterId === actorId) { - await requireCustomer(ctx, actorId, ticketDoc.tenantId) + if (normalizedRole === "CUSTOMER") { + await requireCustomer(ctx, actorId, ticketDoc.tenantId) + } else if (STAFF_ROLES.has(normalizedRole)) { + await requireTicketStaff(ctx, actorId, ticketDoc) + } else { + throw new ConvexError("Autor não possui permissão para alterar anexos") + } } else { await requireTicketStaff(ctx, actorId, ticketDoc) } @@ -800,7 +821,6 @@ export const removeCommentAttachment = mutation({ updatedAt: now, }); - const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null; await ctx.db.insert("ticketEvents", { ticketId, type: "ATTACHMENT_REMOVED", diff --git a/src/app/tickets/[id]/page.tsx b/src/app/tickets/[id]/page.tsx index 40286c6..1cb2583 100644 --- a/src/app/tickets/[id]/page.tsx +++ b/src/app/tickets/[id]/page.tsx @@ -2,10 +2,10 @@ import { AppShell } from "@/components/app-shell" import { SiteHeader } from "@/components/site-header" import { TicketDetailView } from "@/components/tickets/ticket-detail-view" import { TicketDetailStatic } from "@/components/tickets/ticket-detail-static" -import { NewTicketDialog } from "@/components/tickets/new-ticket-dialog" +import { NewTicketDialogDeferred } from "@/components/tickets/new-ticket-dialog.client" import { getTicketById } from "@/lib/mocks/tickets" import type { TicketWithDetails } from "@/lib/schemas/ticket" - + type TicketDetailPageProps = { params: Promise<{ id: string }> } @@ -22,7 +22,7 @@ export default async function TicketDetailPage({ params }: TicketDetailPageProps title={`Ticket #${id}`} lead={"Detalhes do ticket"} secondaryAction={Compartilhar} - primaryAction={} + primaryAction={} /> } > diff --git a/src/components/tickets/new-ticket-dialog.client.tsx b/src/components/tickets/new-ticket-dialog.client.tsx new file mode 100644 index 0000000..df315bb --- /dev/null +++ b/src/components/tickets/new-ticket-dialog.client.tsx @@ -0,0 +1,30 @@ +"use client" + +import { useEffect, useState } from "react" + +import { Button } from "@/components/ui/button" + +import { NewTicketDialog } from "./new-ticket-dialog" + +export function NewTicketDialogDeferred() { + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) { + return ( + + ) + } + + return +} diff --git a/src/components/ui/dropzone.tsx b/src/components/ui/dropzone.tsx index 635bd43..e2ec5f0 100644 --- a/src/components/ui/dropzone.tsx +++ b/src/components/ui/dropzone.tsx @@ -31,18 +31,19 @@ export function Dropzone({ const startUpload = useCallback(async (files: FileList | File[]) => { const list = Array.from(files).slice(0, maxFiles); - const url = await generateUrl({}); const uploaded: Uploaded[] = []; for (const file of list) { if (file.size > maxSize) continue; + const url = await generateUrl({}); const id = `${file.name}-${file.size}-${Date.now()}`; const localPreview = file.type.startsWith("image/") ? URL.createObjectURL(file) : undefined; setItems((prev) => [...prev, { id, name: file.name, progress: 0, status: "uploading" }]); await new Promise((resolve) => { - const form = new FormData(); - form.append("file", file); const xhr = new XMLHttpRequest(); xhr.open("POST", url); + if (file.type) { + xhr.setRequestHeader("Content-Type", file.type); + } xhr.upload.onprogress = (e) => { if (!e.lengthComputable) return; const progress = Math.round((e.loaded / e.total) * 100); @@ -66,7 +67,7 @@ export function Dropzone({ setItems((prev) => prev.map((it) => (it.id === id ? { ...it, status: "error" } : it))); resolve(); }; - xhr.send(form); + xhr.send(file); }); } if (uploaded.length) onUploaded?.(uploaded);