From 7fb6c65d9a53bfa8003edf32bbadbe9c4af771a6 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Sat, 8 Nov 2025 00:40:32 -0300 Subject: [PATCH] Fix form template labels and guard admin auth tables --- convex/tickets.ts | 21 +++++- src/app/admin/page.tsx | 157 +++++++++++++++++++++++++---------------- 2 files changed, 113 insertions(+), 65 deletions(-) diff --git a/convex/tickets.ts b/convex/tickets.ts index dc62aca..d2c54fb 100644 --- a/convex/tickets.ts +++ b/convex/tickets.ts @@ -80,6 +80,21 @@ function plainTextLength(html: string): number { } } +function resolveFormTemplateLabel( + templateKey: string | null | undefined, + storedLabel: string | null | undefined +): string | null { + if (storedLabel && storedLabel.trim().length > 0) { + return storedLabel.trim(); + } + const normalizedKey = templateKey?.trim(); + if (!normalizedKey) { + return null; + } + const fallback = TICKET_FORM_CONFIG.find((entry) => entry.key === normalizedKey); + return fallback ? fallback.label : null; +} + function escapeHtml(input: string): string { return input .replace(/&/g, "&") @@ -1281,8 +1296,8 @@ export const list = query({ csatComment: typeof t.csatComment === "string" && t.csatComment.trim().length > 0 ? t.csatComment.trim() : null, csatRatedAt: t.csatRatedAt ?? null, csatRatedBy: t.csatRatedBy ? String(t.csatRatedBy) : null, - formTemplate: t.formTemplate ?? null, - formTemplateLabel: t.formTemplateLabel ?? null, + formTemplate: t.formTemplate ?? null, + formTemplateLabel: resolveFormTemplateLabel(t.formTemplate ?? null, t.formTemplateLabel ?? null), company: company ? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false } : t.companyId || t.companySnapshot @@ -1592,7 +1607,7 @@ export const getById = query({ })), }, formTemplate: t.formTemplate ?? null, - formTemplateLabel: t.formTemplateLabel ?? null, + formTemplateLabel: resolveFormTemplateLabel(t.formTemplate ?? null, t.formTemplateLabel ?? null), chatEnabled: Boolean(t.chatEnabled), relatedTicketIds: Array.isArray(t.relatedTicketIds) ? t.relatedTicketIds.map((id) => String(id)) : [], resolvedWithTicketId: t.resolvedWithTicketId ? String(t.resolvedWithTicketId) : null, diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 8217ac7..af95f90 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,3 +1,5 @@ +import { Prisma } from "@prisma/client" + import { AdminUsersManager } from "@/components/admin/admin-users-manager" import { AppShell } from "@/components/app-shell" import { SiteHeader } from "@/components/site-header" @@ -10,80 +12,111 @@ import { getServerSession } from "@/lib/auth-server" export const runtime = "nodejs" export const dynamic = "force-dynamic" -async function loadUsers() { - const users = await prisma.authUser.findMany({ - orderBy: { createdAt: "desc" }, - select: { - id: true, - email: true, - name: true, - role: true, - tenantId: true, - machinePersona: true, - createdAt: true, - updatedAt: true, - }, - }) +function isMissingAuthTableError(error: unknown, table: string): boolean { + if (!(error instanceof Prisma.PrismaClientKnownRequestError)) { + return false + } + if (error.code !== "P2021" && error.code !== "P2023") { + return false + } + const target = typeof error.meta?.table === "string" ? error.meta.table.toLowerCase() : "" + return target.includes(table.toLowerCase()) +} - const domainUsers = await prisma.user.findMany({ - select: { - email: true, - companyId: true, - company: { - select: { - id: true, - name: true, +async function loadUsers() { + try { + const users = await prisma.authUser.findMany({ + orderBy: { createdAt: "desc" }, + select: { + id: true, + email: true, + name: true, + role: true, + tenantId: true, + machinePersona: true, + createdAt: true, + updatedAt: true, + }, + }) + + if (users.length === 0) { + return [] + } + + const domainUsers = await prisma.user.findMany({ + select: { + email: true, + companyId: true, + company: { + select: { + id: true, + name: true, + }, }, }, - }, - }) + }) - const domainByEmail = new Map( - domainUsers.map( - (user: (typeof domainUsers)[number]): [string, (typeof domainUsers)[number]] => [ - user.email.toLowerCase(), - user, - ] + const domainByEmail = new Map( + domainUsers.map( + (user: (typeof domainUsers)[number]): [string, (typeof domainUsers)[number]] => [ + user.email.toLowerCase(), + user, + ] + ) ) - ) - return users.map((user: (typeof users)[number]) => { - const domain = domainByEmail.get(user.email.toLowerCase()) - const normalizedRole = (normalizeRole(user.role) ?? "agent") as RoleOption - return { - id: user.id, - email: user.email, - name: user.name ?? "", - role: normalizedRole, - tenantId: user.tenantId ?? DEFAULT_TENANT_ID, - createdAt: user.createdAt.toISOString(), - updatedAt: user.updatedAt?.toISOString() ?? null, - companyId: domain?.companyId ?? null, - companyName: domain?.company?.name ?? null, - machinePersona: user.machinePersona ?? null, + return users.map((user: (typeof users)[number]) => { + const domain = domainByEmail.get(user.email.toLowerCase()) + const normalizedRole = (normalizeRole(user.role) ?? "agent") as RoleOption + return { + id: user.id, + email: user.email, + name: user.name ?? "", + role: normalizedRole, + tenantId: user.tenantId ?? DEFAULT_TENANT_ID, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt?.toISOString() ?? null, + companyId: domain?.companyId ?? null, + companyName: domain?.company?.name ?? null, + machinePersona: user.machinePersona ?? null, + } + }) + } catch (error) { + if (isMissingAuthTableError(error, "AuthUser")) { + console.warn("[admin] auth tables missing; returning empty user list") + return [] } - }) + throw error + } } async function loadInvites(): Promise { - const invites = await prisma.authInvite.findMany({ - orderBy: { createdAt: "desc" }, - include: { - events: { - orderBy: { createdAt: "asc" }, + try { + const invites = await prisma.authInvite.findMany({ + orderBy: { createdAt: "desc" }, + include: { + events: { + orderBy: { createdAt: "asc" }, + }, }, - }, - }) - - const now = new Date() - const cutoff = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) - return invites - .map((invite: (typeof invites)[number]) => normalizeInvite(invite, now)) - .filter((invite: NormalizedInvite) => { - if (invite.status !== "revoked") return true - if (!invite.revokedAt) return true - return new Date(invite.revokedAt) > cutoff }) + + const now = new Date() + const cutoff = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) + return invites + .map((invite) => normalizeInvite(invite, now)) + .filter((invite: NormalizedInvite) => { + if (invite.status !== "revoked") return true + if (!invite.revokedAt) return true + return new Date(invite.revokedAt) > cutoff + }) + } catch (error) { + if (isMissingAuthTableError(error, "AuthInvite")) { + console.warn("[admin] auth invite tables missing; returning empty invite list") + return [] + } + throw error + } } export default async function AdminPage() {