import sanitizeHtml from "sanitize-html" import { ConvexError, v } from "convex/values" import { mutation, query } from "./_generated/server" import { requireStaff } from "./rbac" const SANITIZE_OPTIONS: sanitizeHtml.IOptions = { allowedTags: [ "p", "br", "a", "strong", "em", "u", "s", "blockquote", "ul", "ol", "li", "code", "pre", "span", "h1", "h2", "h3", ], allowedAttributes: { a: ["href", "name", "target", "rel"], span: ["style"], code: ["class"], pre: ["class"], }, allowedSchemes: ["http", "https", "mailto"], transformTags: { a: sanitizeHtml.simpleTransform("a", { rel: "noopener noreferrer", target: "_blank" }, true), }, allowVulnerableTags: false, } function sanitizeTemplateBody(body: string) { const sanitized = sanitizeHtml(body || "", SANITIZE_OPTIONS).trim() return sanitized } function normalizeTitle(title: string) { return title?.trim() } export const list = query({ args: { tenantId: v.string(), viewerId: v.id("users"), kind: v.optional(v.string()), }, handler: async (ctx, { tenantId, viewerId, kind }) => { await requireStaff(ctx, viewerId, tenantId) const normalizedKind = (kind ?? "comment").toLowerCase() const templates = await ctx.db .query("commentTemplates") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .take(100) return templates .filter((template) => (template.kind ?? "comment") === normalizedKind) .sort((a, b) => a.title.localeCompare(b.title, "pt-BR", { sensitivity: "base" })) .map((template) => ({ id: template._id, title: template.title, body: template.body, kind: template.kind ?? "comment", createdAt: template.createdAt, updatedAt: template.updatedAt, createdBy: template.createdBy, updatedBy: template.updatedBy ?? null, })) }, }) export const create = mutation({ args: { tenantId: v.string(), actorId: v.id("users"), title: v.string(), body: v.string(), kind: v.optional(v.string()), }, handler: async (ctx, { tenantId, actorId, title, body, kind }) => { await requireStaff(ctx, actorId, tenantId) const normalizedTitle = normalizeTitle(title) if (!normalizedTitle || normalizedTitle.length < 3) { throw new ConvexError("Informe um título válido para o template") } const sanitizedBody = sanitizeTemplateBody(body) if (!sanitizedBody) { throw new ConvexError("Informe o conteúdo do template") } const normalizedKind = (kind ?? "comment").toLowerCase() const existing = await ctx.db .query("commentTemplates") .withIndex("by_tenant_title", (q) => q.eq("tenantId", tenantId).eq("title", normalizedTitle)) .first() if (existing && (existing.kind ?? "comment") === normalizedKind) { throw new ConvexError("Já existe um template com este título") } const now = Date.now() const id = await ctx.db.insert("commentTemplates", { tenantId, kind: normalizedKind, title: normalizedTitle, body: sanitizedBody, createdBy: actorId, updatedBy: actorId, createdAt: now, updatedAt: now, }) return id }, }) export const update = mutation({ args: { templateId: v.id("commentTemplates"), tenantId: v.string(), actorId: v.id("users"), title: v.string(), body: v.string(), kind: v.optional(v.string()), }, handler: async (ctx, { templateId, tenantId, actorId, title, body, kind }) => { await requireStaff(ctx, actorId, tenantId) const template = await ctx.db.get(templateId) if (!template || template.tenantId !== tenantId) { throw new ConvexError("Template não encontrado") } const normalizedTitle = normalizeTitle(title) if (!normalizedTitle || normalizedTitle.length < 3) { throw new ConvexError("Informe um título válido para o template") } const sanitizedBody = sanitizeTemplateBody(body) if (!sanitizedBody) { throw new ConvexError("Informe o conteúdo do template") } const normalizedKind = (kind ?? "comment").toLowerCase() const duplicate = await ctx.db .query("commentTemplates") .withIndex("by_tenant_title", (q) => q.eq("tenantId", tenantId).eq("title", normalizedTitle)) .first() if (duplicate && duplicate._id !== templateId && (duplicate.kind ?? "comment") === normalizedKind) { throw new ConvexError("Já existe um template com este título") } const now = Date.now() await ctx.db.patch(templateId, { kind: normalizedKind, title: normalizedTitle, body: sanitizedBody, updatedBy: actorId, updatedAt: now, }) }, }) export const remove = mutation({ args: { templateId: v.id("commentTemplates"), tenantId: v.string(), actorId: v.id("users"), }, handler: async (ctx, { templateId, tenantId, actorId }) => { await requireStaff(ctx, actorId, tenantId) const template = await ctx.db.get(templateId) if (!template || template.tenantId !== tenantId) { throw new ConvexError("Template não encontrado") } await ctx.db.delete(templateId) }, })