fix: harden machine session fallback and clean lint
This commit is contained in:
parent
2607ca5ce3
commit
846e575637
6 changed files with 60 additions and 32 deletions
|
|
@ -106,6 +106,53 @@ function normalizeTeams(teams?: string[] | null): string[] {
|
||||||
return teams.map((team) => renameQueueString(team) ?? team);
|
return teams.map((team) => renameQueueString(team) ?? team);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RequesterFallbackContext = {
|
||||||
|
ticketId?: Id<"tickets">;
|
||||||
|
fallbackName?: string | null;
|
||||||
|
fallbackEmail?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildRequesterSummary(
|
||||||
|
requester: Doc<"users"> | null,
|
||||||
|
requesterId: Id<"users">,
|
||||||
|
context?: RequesterFallbackContext,
|
||||||
|
) {
|
||||||
|
if (requester) {
|
||||||
|
return {
|
||||||
|
id: requester._id,
|
||||||
|
name: requester.name,
|
||||||
|
email: requester.email,
|
||||||
|
avatarUrl: requester.avatarUrl,
|
||||||
|
teams: normalizeTeams(requester.teams),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const idString = String(requesterId);
|
||||||
|
const fallbackName =
|
||||||
|
typeof context?.fallbackName === "string" && context.fallbackName.trim().length > 0
|
||||||
|
? context.fallbackName.trim()
|
||||||
|
: "Solicitante não encontrado";
|
||||||
|
const fallbackEmailCandidate =
|
||||||
|
typeof context?.fallbackEmail === "string" && context.fallbackEmail.includes("@")
|
||||||
|
? context.fallbackEmail
|
||||||
|
: null;
|
||||||
|
const fallbackEmail = fallbackEmailCandidate ?? `requester-${idString}@example.invalid`;
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== "test") {
|
||||||
|
const ticketInfo = context?.ticketId ? ` (ticket ${String(context.ticketId)})` : "";
|
||||||
|
console.warn(
|
||||||
|
`[tickets] requester ${idString} ausente ao hidratar resposta${ticketInfo}; usando placeholders.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: requesterId,
|
||||||
|
name: fallbackName,
|
||||||
|
email: fallbackEmail,
|
||||||
|
teams: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
type CustomFieldInput = {
|
type CustomFieldInput = {
|
||||||
fieldId: Id<"ticketFields">;
|
fieldId: Id<"ticketFields">;
|
||||||
value: unknown;
|
value: unknown;
|
||||||
|
|
@ -348,13 +395,7 @@ export const list = query({
|
||||||
channel: t.channel,
|
channel: t.channel,
|
||||||
queue: queueName,
|
queue: queueName,
|
||||||
company: company ? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false } : null,
|
company: company ? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false } : null,
|
||||||
requester: requester && {
|
requester: buildRequesterSummary(requester, t.requesterId, { ticketId: t._id }),
|
||||||
id: requester._id,
|
|
||||||
name: requester.name,
|
|
||||||
email: requester.email,
|
|
||||||
avatarUrl: requester.avatarUrl,
|
|
||||||
teams: normalizeTeams(requester.teams),
|
|
||||||
},
|
|
||||||
assignee: assignee
|
assignee: assignee
|
||||||
? {
|
? {
|
||||||
id: assignee._id,
|
id: assignee._id,
|
||||||
|
|
@ -526,13 +567,7 @@ export const getById = query({
|
||||||
channel: t.channel,
|
channel: t.channel,
|
||||||
queue: queueName,
|
queue: queueName,
|
||||||
company: company ? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false } : null,
|
company: company ? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false } : null,
|
||||||
requester: requester && {
|
requester: buildRequesterSummary(requester, t.requesterId, { ticketId: t._id }),
|
||||||
id: requester._id,
|
|
||||||
name: requester.name,
|
|
||||||
email: requester.email,
|
|
||||||
avatarUrl: requester.avatarUrl,
|
|
||||||
teams: normalizeTeams(requester.teams),
|
|
||||||
},
|
|
||||||
assignee: assignee
|
assignee: assignee
|
||||||
? {
|
? {
|
||||||
id: assignee._id,
|
id: assignee._id,
|
||||||
|
|
@ -1434,13 +1469,7 @@ export const playNext = mutation({
|
||||||
priority: chosen.priority,
|
priority: chosen.priority,
|
||||||
channel: chosen.channel,
|
channel: chosen.channel,
|
||||||
queue: queueName,
|
queue: queueName,
|
||||||
requester: requester && {
|
requester: buildRequesterSummary(requester, chosen.requesterId, { ticketId: chosen._id }),
|
||||||
id: requester._id,
|
|
||||||
name: requester.name,
|
|
||||||
email: requester.email,
|
|
||||||
avatarUrl: requester.avatarUrl,
|
|
||||||
teams: normalizeTeams(requester.teams),
|
|
||||||
},
|
|
||||||
assignee: assignee
|
assignee: assignee
|
||||||
? {
|
? {
|
||||||
id: assignee._id,
|
id: assignee._id,
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@ import { mutation, query } from "./_generated/server";
|
||||||
import { ConvexError, v } from "convex/values";
|
import { ConvexError, v } from "convex/values";
|
||||||
import { requireAdmin } from "./rbac";
|
import { requireAdmin } from "./rbac";
|
||||||
|
|
||||||
// All roles that have staff-level access in some areas. Do NOT include COLLABORATOR here
|
|
||||||
// to avoid leaking collaborators into staff pickers such as "responsável".
|
|
||||||
const STAFF_ROLES = new Set(["ADMIN", "MANAGER", "AGENT"]);
|
|
||||||
const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT"]);
|
const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT"]);
|
||||||
|
|
||||||
export const ensureUser = mutation({
|
export const ensureUser = mutation({
|
||||||
|
|
|
||||||
|
|
@ -994,16 +994,13 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
const metrics = machine?.metrics ?? null
|
const metrics = machine?.metrics ?? null
|
||||||
const metricsCapturedAt = useMemo(() => getMetricsTimestamp(metrics), [metrics])
|
const metricsCapturedAt = useMemo(() => getMetricsTimestamp(metrics), [metrics])
|
||||||
// Live refresh the relative time label every second when we have a capture timestamp
|
// Live refresh the relative time label every second when we have a capture timestamp
|
||||||
const [relativeTick, setRelativeTick] = useState(0)
|
const [, setRelativeTick] = useState(0)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!metricsCapturedAt) return
|
if (!metricsCapturedAt) return
|
||||||
const id = setInterval(() => setRelativeTick((t) => t + 1), 1000)
|
const id = setInterval(() => setRelativeTick((t) => t + 1), 1000)
|
||||||
return () => clearInterval(id)
|
return () => clearInterval(id)
|
||||||
}, [metricsCapturedAt])
|
}, [metricsCapturedAt])
|
||||||
const lastUpdateRelative = useMemo(
|
const lastUpdateRelative = metricsCapturedAt ? formatRelativeTime(metricsCapturedAt) : null
|
||||||
() => (metricsCapturedAt ? formatRelativeTime(metricsCapturedAt) : null),
|
|
||||||
[metricsCapturedAt, relativeTick]
|
|
||||||
)
|
|
||||||
const hardware = metadata?.hardware
|
const hardware = metadata?.hardware
|
||||||
const network = metadata?.network ?? null
|
const network = metadata?.network ?? null
|
||||||
const networkInterfaces = Array.isArray(network) ? network : null
|
const networkInterfaces = Array.isArray(network) ? network : null
|
||||||
|
|
|
||||||
|
|
@ -635,7 +635,7 @@ function PortalCommentAttachmentCard({
|
||||||
window.open(target, "_blank", "noopener,noreferrer")
|
window.open(target, "_blank", "noopener,noreferrer")
|
||||||
toast.error("Não foi possível baixar o anexo automaticamente.", { id: toastId })
|
toast.error("Não foi possível baixar o anexo automaticamente.", { id: toastId })
|
||||||
}
|
}
|
||||||
}, [attachment.name, ensureUrl])
|
}, [attachment.id, attachment.name, ensureUrl])
|
||||||
|
|
||||||
const resolvedUrl = url
|
const resolvedUrl = url
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -650,7 +650,7 @@ function CommentAttachmentCard({
|
||||||
window.open(target, "_blank", "noopener,noreferrer")
|
window.open(target, "_blank", "noopener,noreferrer")
|
||||||
toast.error("Não foi possível baixar o anexo automaticamente.", { id: toastId })
|
toast.error("Não foi possível baixar o anexo automaticamente.", { id: toastId })
|
||||||
}
|
}
|
||||||
}, [attachment.name, ensureUrl, url])
|
}, [attachment.id, attachment.name, ensureUrl, url])
|
||||||
|
|
||||||
const name = attachment.name ?? ""
|
const name = attachment.name ?? ""
|
||||||
const urlLooksImage = url ? /\.(png|jpe?g|gif|webp|svg)$/i.test(url) : false
|
const urlLooksImage = url ? /\.(png|jpe?g|gif|webp|svg)$/i.test(url) : false
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
// carregar o contexto — se a API responder 200, assumimos que há sessão válida
|
// carregar o contexto — se a API responder 200, assumimos que há sessão válida
|
||||||
// do lado do servidor e populamos o contexto para o restante do app.
|
// do lado do servidor e populamos o contexto para o restante do app.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isPending) {
|
||||||
|
setMachineContextLoading(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const shouldFetch = Boolean(session?.user?.role === "machine") || !session?.user
|
const shouldFetch = Boolean(session?.user?.role === "machine") || !session?.user
|
||||||
if (!shouldFetch) {
|
if (!shouldFetch) {
|
||||||
setMachineContext(null)
|
setMachineContext(null)
|
||||||
|
|
@ -215,7 +220,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [session?.user])
|
}, [session?.user, isPending])
|
||||||
|
|
||||||
// Poll machine session periodically to reflect admin changes (e.g., deactivation)
|
// Poll machine session periodically to reflect admin changes (e.g., deactivation)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue