fix: pre-landing review fixes — company_id filter and CW condition injection

- Apply company_id filter in CW search_tickets conditions (was silently ignored)
- Sanitize query string to strip single quotes before CW condition interpolation
- Add psaError state to TicketsPage for permissions error surfacing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 14:42:05 +00:00
parent fb7690485b
commit 294b309faa
2 changed files with 28 additions and 4 deletions

View File

@@ -74,7 +74,9 @@ class ConnectWiseProvider(PSAProvider):
conditions: list[str] = []
if query:
conditions.append(f"summary contains '{query}'")
# Sanitize: strip single quotes to prevent CW condition injection
safe_query = query.replace("'", "")
conditions.append(f"summary contains '{safe_query}'")
if filters.get("board_id"):
conditions.append(f"board/id = {filters['board_id']}")
if filters.get("status_id"):
@@ -89,6 +91,8 @@ class ConnectWiseProvider(PSAProvider):
if board_ids:
board_list = ", ".join(str(bid) for bid in board_ids)
conditions.append(f"board/id in ({board_list})")
if filters.get("company_id"):
conditions.append(f"company/id = {int(filters['company_id'])}")
condition_str = " and ".join(conditions) if conditions else ""
if condition_str:

View File

@@ -1,6 +1,7 @@
import { useEffect, useState, useCallback } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Plus, Ticket } from 'lucide-react'
import { Plus, Ticket, AlertTriangle } from 'lucide-react'
import axios from 'axios'
import { TicketFilterBar } from '@/components/tickets/TicketFilterBar'
import { TicketListRow } from '@/components/tickets/TicketListRow'
import { TicketDetailPanel } from '@/components/tickets/TicketDetailPanel'
@@ -37,6 +38,7 @@ export default function TicketsPage() {
const [tickets, setTickets] = useState<PSATicketSearchResult[]>([])
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(false)
const [psaError, setPsaError] = useState<string | null>(null)
const [boards, setBoards] = useState<PSABoard[]>([])
const [statuses, setStatuses] = useState<PSATicketStatusItem[]>([])
const [priorities, setPriorities] = useState<PSAPriority[]>([])
@@ -66,6 +68,7 @@ export default function TicketsPage() {
// Fetch tickets on filter/page change
const fetchTickets = useCallback(async () => {
setLoading(true)
setPsaError(null)
try {
const result = await ticketsApi.searchTickets({
query: filters.search || undefined,
@@ -90,9 +93,20 @@ export default function TicketsPage() {
})
return seen.size > 0 ? Array.from(seen, ([id, name]) => ({ id, name })) : prev
})
} catch {
} catch (err: unknown) {
setTickets([])
setTotal(0)
if (axios.isAxiosError(err)) {
const status = err.response?.status
const detail = (err.response?.data as { detail?: string })?.detail ?? ''
if (status === 502 && detail.toLowerCase().includes('permission')) {
setPsaError('ConnectWise returned a permissions error. Check that the API member\'s security role has Service Tickets → Inquire → ALL and System → Table Setup → Inquire → ALL.')
} else if (status === 502) {
setPsaError('ConnectWise is unavailable or returned an error. Check your integration settings.')
} else {
setPsaError('Failed to load tickets.')
}
}
} finally {
setLoading(false)
}
@@ -165,7 +179,13 @@ export default function TicketsPage() {
Loading tickets
</div>
)}
{!loading && tickets.length === 0 && (
{!loading && psaError && (
<div className="mx-6 mt-6 flex items-start gap-3 px-4 py-3 rounded-lg bg-danger-dim border border-danger/30 text-sm text-danger">
<AlertTriangle className="w-4 h-4 mt-0.5 shrink-0" />
<span>{psaError}</span>
</div>
)}
{!loading && !psaError && tickets.length === 0 && (
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground text-sm gap-2">
<Ticket className="w-8 h-8 opacity-30" />
No tickets match your filters