Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion web/default/src/features/keys/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export async function getApiKeys(
// Search API keys by keyword or token (with pagination)
export async function searchApiKeys(
params: SearchApiKeysParams
): Promise<{ success: boolean; message?: string; data?: ApiKey[] }> {
): Promise<GetApiKeysResponse> {
const { keyword = '', token = '', p, size } = params
const queryParams = new URLSearchParams()
if (keyword) queryParams.set('keyword', keyword)
Expand Down
96 changes: 62 additions & 34 deletions web/default/src/features/keys/components/api-keys-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table'
import { useDebounce } from '@/hooks'
import { Database } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
Expand All @@ -43,6 +44,7 @@ import {
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
import { Input } from '@/components/ui/input'
import { Skeleton } from '@/components/ui/skeleton'
import {
DISABLED_ROW_DESKTOP,
Expand Down Expand Up @@ -208,9 +210,35 @@ export function ApiKeysTable() {
navigate: route.useNavigate(),
pagination: { defaultPage: 1, defaultPageSize: 20 },
globalFilter: { enabled: true, key: 'filter' },
columnFilters: [{ columnId: 'status', searchKey: 'status', type: 'array' }],
columnFilters: [
{ columnId: 'status', searchKey: 'status', type: 'array' },
{ columnId: '_tokenSearch', searchKey: 'token', type: 'string' },
],
Comment on lines +213 to +216
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid persisting API key token filters in URL state.

Syncing _tokenSearch to URL query params can leak sensitive API key fragments via browser history, shared links, and referrer logs. Keep token search input in local component state only, and pass it to the query function without route synchronization.

Suggested change
   columnFilters: [
     { columnId: 'status', searchKey: 'status', type: 'array' },
-    { columnId: '_tokenSearch', searchKey: 'token', type: 'string' },
   ],

-  const tokenFilterFromUrl =
-    (columnFilters.find((f) => f.id === '_tokenSearch')?.value as string) || ''
-  const [tokenFilterInput, setTokenFilterInput] = useState(tokenFilterFromUrl)
+  const [tokenFilterInput, setTokenFilterInput] = useState('')
   const debouncedTokenFilter = useDebounce(tokenFilterInput, 500)

-  useEffect(() => {
-    setTokenFilterInput(tokenFilterFromUrl)
-  }, [tokenFilterFromUrl])
-
-  useEffect(() => {
-    if (debouncedTokenFilter !== tokenFilterFromUrl) {
-      onColumnFiltersChange((prev) => {
-        const filtered = prev.filter((f) => f.id !== '_tokenSearch')
-        return debouncedTokenFilter
-          ? [...filtered, { id: '_tokenSearch', value: debouncedTokenFilter }]
-          : filtered
-      })
-    }
-  }, [debouncedTokenFilter, tokenFilterFromUrl, onColumnFiltersChange])
-
-  const tokenFilter = tokenFilterFromUrl
+  const tokenFilter = debouncedTokenFilter.trim()

As per coding guidelines, "do not store sensitive information in frontend".

Also applies to: 219-237

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/keys/components/api-keys-table.tsx` around lines 213
- 216, Remove the sensitive "_tokenSearch" entry from the URL-synced
columnFilters and stop persisting it to route/query params; instead keep the
token search value in local component state within the ApiKeysTable component
and pass that local value into the data-fetch/query function (where
columnFilters or searchKey 'token' is consumed) so filtering still works without
writing the token to the URL or router state; search the file for
"_tokenSearch", the columnFilters array and any code that syncs column state to
query params and remove/replace those URL syncs so only non-sensitive filters
remain in columnFilters.

})

const tokenFilterFromUrl =
(columnFilters.find((f) => f.id === '_tokenSearch')?.value as string) || ''
const [tokenFilterInput, setTokenFilterInput] = useState(tokenFilterFromUrl)
const debouncedTokenFilter = useDebounce(tokenFilterInput, 500)

useEffect(() => {
setTokenFilterInput(tokenFilterFromUrl)
}, [tokenFilterFromUrl])

useEffect(() => {
if (debouncedTokenFilter !== tokenFilterFromUrl) {
onColumnFiltersChange((prev) => {
const filtered = prev.filter((f) => f.id !== '_tokenSearch')
return debouncedTokenFilter
? [...filtered, { id: '_tokenSearch', value: debouncedTokenFilter }]
: filtered
})
}
}, [debouncedTokenFilter, tokenFilterFromUrl, onColumnFiltersChange])

const tokenFilter = tokenFilterFromUrl
const shouldSearch = Boolean(globalFilter?.trim() || tokenFilter.trim())

// Fetch data with React Query
// eslint-disable-next-line @tanstack/query/exhaustive-deps
const { data, isLoading, isFetching } = useQuery({
Expand All @@ -219,32 +247,31 @@ export function ApiKeysTable() {
pagination.pageIndex + 1,
pagination.pageSize,
globalFilter,
tokenFilter,
refreshTrigger,
],
queryFn: async () => {
// If there's a global filter, use search
const hasFilter = globalFilter?.trim()

if (hasFilter) {
const result = await searchApiKeys({ keyword: globalFilter })
if (!result.success) {
toast.error(result.message || t(ERROR_MESSAGES.SEARCH_FAILED))
return { items: [], total: 0 }
}
return {
items: result.data || [],
total: result.data?.length || 0,
}
}

// Otherwise use pagination
const result = await getApiKeys({
p: pagination.pageIndex + 1,
size: pagination.pageSize,
})
const result = shouldSearch
? await searchApiKeys({
keyword: globalFilter,
token: tokenFilter,
p: pagination.pageIndex + 1,
size: pagination.pageSize,
})
: await getApiKeys({
p: pagination.pageIndex + 1,
size: pagination.pageSize,
})

if (!result.success) {
toast.error(result.message || t(ERROR_MESSAGES.LOAD_FAILED))
toast.error(
result.message ||
t(
shouldSearch
? ERROR_MESSAGES.SEARCH_FAILED
: ERROR_MESSAGES.LOAD_FAILED
)
)
return { items: [], total: 0 }
}

Expand Down Expand Up @@ -273,13 +300,7 @@ export function ApiKeysTable() {
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnVisibilityChange: setColumnVisibility,
globalFilterFn: (row, _columnId, filterValue) => {
const name = String(row.getValue('name')).toLowerCase()
const key = String(row.original.key).toLowerCase()
const searchValue = String(filterValue).toLowerCase()

return name.includes(searchValue) || key.includes(searchValue)
},
globalFilterFn: () => true,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
Expand All @@ -289,10 +310,8 @@ export function ApiKeysTable() {
onPaginationChange,
onGlobalFilterChange,
onColumnFiltersChange,
manualPagination: !globalFilter,
pageCount: globalFilter
? Math.ceil((data?.total || 0) / pagination.pageSize)
: Math.ceil((data?.total || 0) / pagination.pageSize),
manualPagination: true,
pageCount: Math.ceil((data?.total || 0) / pagination.pageSize),
})

const pageCount = table.getPageCount()
Expand All @@ -312,7 +331,16 @@ export function ApiKeysTable() {
)}
skeletonKeyPrefix='api-keys-skeleton'
toolbarProps={{
searchPlaceholder: t('Filter by name or key...'),
searchPlaceholder: t('Filter by name...'),
additionalSearch: (
<Input
placeholder={t('Filter by API key...')}
aria-label={t('Filter by API key...')}
value={tokenFilterInput}
onChange={(e) => setTokenFilterInput(e.target.value)}
className='w-full sm:w-50 lg:w-60'
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
),
filters: [
{
columnId: 'status',
Expand Down
3 changes: 2 additions & 1 deletion web/default/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1729,8 +1729,9 @@
"Filter by Midjourney task ID": "Filter by Midjourney task ID",
"Filter by model name...": "Filter by model name...",
"Filter by model...": "Filter by model...",
"Filter by API key...": "Filter by API key...",
"Filter by name or ID...": "Filter by name or ID...",
"Filter by name or key...": "Filter by name or key...",
"Filter by name...": "Filter by name...",
"Filter by name, ID, or key...": "Filter by name, ID, or key...",
"Filter by price field": "Filter by price field",
"Filter by ratio type": "Filter by ratio type",
Expand Down
3 changes: 2 additions & 1 deletion web/default/src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1729,8 +1729,9 @@
"Filter by Midjourney task ID": "Filtrer par ID de tâche Midjourney",
"Filter by model name...": "Filtrer par nom du modèle...",
"Filter by model...": "Filtrer par modèle...",
"Filter by API key...": "Filtrer par clé API...",
"Filter by name or ID...": "Filtrer par nom ou ID...",
"Filter by name or key...": "Filtrer par nom ou clé...",
"Filter by name...": "Filtrer par nom...",
"Filter by name, ID, or key...": "Filtrer par nom, ID ou clé...",
"Filter by price field": "Filtrer par champ de prix",
"Filter by ratio type": "Filtrer par type de ratio",
Expand Down
3 changes: 2 additions & 1 deletion web/default/src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1729,8 +1729,9 @@
"Filter by Midjourney task ID": "MidjourneyタスクIDでフィルター",
"Filter by model name...": "モデル名でフィルター...",
"Filter by model...": "モデルでフィルタリング...",
"Filter by API key...": "APIキーでフィルター...",
"Filter by name or ID...": "名前またはIDでフィルター...",
"Filter by name or key...": "名前またはキーでフィルター...",
"Filter by name...": "名前でフィルター...",
"Filter by name, ID, or key...": "名前、ID、またはキーでフィルター...",
"Filter by price field": "価格フィールドでフィルター",
"Filter by ratio type": "倍率タイプで絞り込み",
Expand Down
3 changes: 2 additions & 1 deletion web/default/src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1729,8 +1729,9 @@
"Filter by Midjourney task ID": "Фильтр по ID задачи Midjourney",
"Filter by model name...": "Фильтр по имени модели...",
"Filter by model...": "Фильтровать по модели...",
"Filter by API key...": "Фильтр по API-ключу...",
"Filter by name or ID...": "Фильтр по имени или ID...",
"Filter by name or key...": "Фильтровать по имени или ключу...",
"Filter by name...": "Фильтр по имени...",
"Filter by name, ID, or key...": "Фильтровать по имени, ID или ключу...",
"Filter by price field": "Фильтр по полю цены",
"Filter by ratio type": "Фильтровать по типу коэффициента",
Expand Down
3 changes: 2 additions & 1 deletion web/default/src/i18n/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1729,8 +1729,9 @@
"Filter by Midjourney task ID": "Lọc theo ID nhiệm vụ Midjourney",
"Filter by model name...": "Lọc theo tên mô hình...",
"Filter by model...": "Lọc theo mẫu...",
"Filter by API key...": "Lọc theo khóa API...",
"Filter by name or ID...": "Lọc theo tên hoặc ID...",
"Filter by name or key...": "Lọc theo tên hoặc khóa...",
"Filter by name...": "Lọc theo tên...",
"Filter by name, ID, or key...": "Lọc theo tên, ID hoặc khóa...",
"Filter by price field": "Lọc theo trường giá",
"Filter by ratio type": "Lọc theo loại tỷ lệ",
Expand Down
3 changes: 2 additions & 1 deletion web/default/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1729,8 +1729,9 @@
"Filter by Midjourney task ID": "按 Midjourney 任务 ID 筛选",
"Filter by model name...": "按模型名称筛选...",
"Filter by model...": "按模型筛选...",
"Filter by API key...": "按 API 密钥筛选...",
"Filter by name or ID...": "按名称或 ID 筛选...",
"Filter by name or key...": "按名称或密钥筛选...",
"Filter by name...": "按名称筛选...",
"Filter by name, ID, or key...": "按名称、ID 或密钥筛选...",
"Filter by price field": "按价格字段筛选",
"Filter by ratio type": "按倍率类型筛选",
Expand Down
1 change: 1 addition & 0 deletions web/default/src/routes/_authenticated/keys/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const apiKeySearchSchema = z.object({
.optional()
.catch([]),
filter: z.string().optional().catch(''),
token: z.string().optional().catch(''),
})

export const Route = createFileRoute('/_authenticated/keys/')({
Expand Down