feat: better admin permissions (#5755)
* feat: add casbin admin permissions * feat: improve audit logging to associate logs with actual operators and target users * feat: enhance admin permissions and UI interactions for sensitive actions * Refactor authz RBAC and tighten channel permissions * Split channel authz field policy * Address channel authz review findings
This commit is contained in:
+42
-11
@@ -32,6 +32,12 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
ADMIN_PERMISSION_ACTIONS,
|
||||
ADMIN_PERMISSION_RESOURCES,
|
||||
hasPermission,
|
||||
} from '@/lib/admin-permissions'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -43,6 +49,11 @@ import {
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import {
|
||||
handleDeleteAllDisabled,
|
||||
@@ -65,6 +76,12 @@ export function ChannelsPrimaryButtons() {
|
||||
} = useChannels()
|
||||
const queryClient = useQueryClient()
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const currentUser = useAuthStore((s) => s.auth.user)
|
||||
const canEditSensitive = hasPermission(
|
||||
currentUser,
|
||||
ADMIN_PERMISSION_RESOURCES.CHANNEL,
|
||||
ADMIN_PERMISSION_ACTIONS.SENSITIVE_WRITE
|
||||
)
|
||||
|
||||
const handleTagModeToggle = (checked: boolean) => {
|
||||
localStorage.setItem('enable-tag-mode', String(checked))
|
||||
@@ -105,17 +122,28 @@ export function ChannelsPrimaryButtons() {
|
||||
</div>
|
||||
|
||||
{/* Create Channel */}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setCurrentRow(null)
|
||||
setOpen('create-channel')
|
||||
}}
|
||||
size='sm'
|
||||
>
|
||||
<Plus className='h-4 w-4' />
|
||||
<span className='max-sm:hidden'>{t('Create Channel')}</span>
|
||||
<span className='sm:hidden'>{t('Create')}</span>
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className='inline-flex' />}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!canEditSensitive) return
|
||||
setCurrentRow(null)
|
||||
setOpen('create-channel')
|
||||
}}
|
||||
size='sm'
|
||||
disabled={!canEditSensitive}
|
||||
>
|
||||
<Plus className='h-4 w-4' />
|
||||
<span className='max-sm:hidden'>{t('Create Channel')}</span>
|
||||
<span className='sm:hidden'>{t('Create')}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{!canEditSensitive && (
|
||||
<TooltipContent>
|
||||
{t('No permission to perform this action')}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
{/* More Actions */}
|
||||
<DropdownMenu>
|
||||
@@ -209,8 +237,10 @@ export function ChannelsPrimaryButtons() {
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
if (!canEditSensitive) return
|
||||
setShowDeleteDialog(true)
|
||||
}}
|
||||
disabled={!canEditSensitive}
|
||||
className='text-destructive focus:text-destructive'
|
||||
>
|
||||
{t('Delete All Disabled')}
|
||||
@@ -231,6 +261,7 @@ export function ChannelsPrimaryButtons() {
|
||||
)}
|
||||
destructive
|
||||
handleConfirm={() => {
|
||||
if (!canEditSensitive) return
|
||||
handleDeleteAllDisabled(queryClient, (_count) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Deleted ${_count} channels`)
|
||||
|
||||
@@ -24,6 +24,13 @@ import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import {
|
||||
ADMIN_PERMISSION_ACTIONS,
|
||||
ADMIN_PERMISSION_RESOURCES,
|
||||
hasPermission,
|
||||
} from '@/lib/admin-permissions'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -51,6 +58,12 @@ export function DataTableBulkActions<TData>({
|
||||
const [showTagDialog, setShowTagDialog] = useState(false)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const [tagValue, setTagValue] = useState('')
|
||||
const currentUser = useAuthStore((s) => s.auth.user)
|
||||
const canEditSensitive = hasPermission(
|
||||
currentUser,
|
||||
ADMIN_PERMISSION_RESOURCES.CHANNEL,
|
||||
ADMIN_PERMISSION_ACTIONS.SENSITIVE_WRITE
|
||||
)
|
||||
|
||||
const selectedRows = table.getFilteredSelectedRowModel().rows
|
||||
const selectedIds = selectedRows.reduce<number[]>((ids, row) => {
|
||||
@@ -76,6 +89,7 @@ export function DataTableBulkActions<TData>({
|
||||
}
|
||||
|
||||
const handleDeleteAll = () => {
|
||||
if (!canEditSensitive) return
|
||||
handleBatchDelete(selectedIds, queryClient, () => {
|
||||
setShowDeleteConfirm(false)
|
||||
handleClearSelection()
|
||||
@@ -164,10 +178,21 @@ export function DataTableBulkActions<TData>({
|
||||
<Button
|
||||
variant='destructive'
|
||||
size='icon'
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className='size-8'
|
||||
onClick={() => {
|
||||
if (!canEditSensitive) return
|
||||
setShowDeleteConfirm(true)
|
||||
}}
|
||||
aria-disabled={!canEditSensitive}
|
||||
className={cn(
|
||||
'size-8',
|
||||
!canEditSensitive && 'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
aria-label={t('Delete selected channels')}
|
||||
title={t('Delete selected channels')}
|
||||
title={
|
||||
canEditSensitive
|
||||
? t('Delete selected channels')
|
||||
: t('No permission to perform this action')
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -175,7 +200,11 @@ export function DataTableBulkActions<TData>({
|
||||
<span className='sr-only'>{t('Delete selected channels')}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('Delete selected channels')}</p>
|
||||
<p>
|
||||
{canEditSensitive
|
||||
? t('Delete selected channels')
|
||||
: t('No permission to perform this action')}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</BulkActionsToolbar>
|
||||
@@ -243,7 +272,11 @@ export function DataTableBulkActions<TData>({
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button variant='destructive' onClick={handleDeleteAll}>
|
||||
<Button
|
||||
variant='destructive'
|
||||
onClick={handleDeleteAll}
|
||||
disabled={!canEditSensitive}
|
||||
>
|
||||
{t('Delete')}
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -39,6 +39,12 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
ADMIN_PERMISSION_ACTIONS,
|
||||
ADMIN_PERMISSION_RESOURCES,
|
||||
hasPermission,
|
||||
} from '@/lib/admin-permissions'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -77,12 +83,18 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
const channel = row.original
|
||||
const { setOpen, setCurrentRow, upstream } = useChannels()
|
||||
const queryClient = useQueryClient()
|
||||
const currentUser = useAuthStore((s) => s.auth.user)
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
|
||||
const [isTesting, setIsTesting] = useState(false)
|
||||
const [isTogglingStatus, setIsTogglingStatus] = useState(false)
|
||||
|
||||
const isEnabled = isChannelEnabled(channel)
|
||||
const isMultiKey = isMultiKeyChannel(channel)
|
||||
const canEditSensitive = hasPermission(
|
||||
currentUser,
|
||||
ADMIN_PERMISSION_RESOURCES.CHANNEL,
|
||||
ADMIN_PERMISSION_ACTIONS.SENSITIVE_WRITE
|
||||
)
|
||||
|
||||
const handleEdit = () => {
|
||||
setCurrentRow(channel)
|
||||
@@ -314,12 +326,20 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* Copy Channel */}
|
||||
<DropdownMenuItem onClick={handleCopy}>
|
||||
<DropdownMenuItem
|
||||
disabled={!canEditSensitive}
|
||||
onClick={canEditSensitive ? handleCopy : undefined}
|
||||
>
|
||||
{t('Copy Channel')}
|
||||
<DropdownMenuShortcut>
|
||||
<Copy size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
{!canEditSensitive && (
|
||||
<DropdownMenuItem disabled className='text-xs normal-case'>
|
||||
{t('No permission to perform this action')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{/* Manage Keys (only for multi-key channels) */}
|
||||
{isMultiKey && (
|
||||
@@ -335,8 +355,10 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
|
||||
{/* Delete */}
|
||||
<DropdownMenuItem
|
||||
disabled={!canEditSensitive}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
if (!canEditSensitive) return
|
||||
setDeleteConfirmOpen(true)
|
||||
}}
|
||||
className='text-destructive focus:text-destructive'
|
||||
@@ -360,6 +382,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
confirmText={t('Delete')}
|
||||
destructive
|
||||
handleConfirm={() => {
|
||||
if (!canEditSensitive) return
|
||||
handleDeleteChannel(channel.id, queryClient)
|
||||
setDeleteConfirmOpen(false)
|
||||
}}
|
||||
|
||||
+36
-1
@@ -21,6 +21,12 @@ import { useQueryClient } from '@tanstack/react-query'
|
||||
import { Loader2, RefreshCw, Trash2, Power, PowerOff } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
ADMIN_PERMISSION_ACTIONS,
|
||||
ADMIN_PERMISSION_RESOURCES,
|
||||
hasPermission,
|
||||
} from '@/lib/admin-permissions'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Select,
|
||||
@@ -69,6 +75,12 @@ export function MultiKeyManageDialog({
|
||||
const { t } = useTranslation()
|
||||
const { currentRow } = useChannels()
|
||||
const queryClient = useQueryClient()
|
||||
const currentUser = useAuthStore((s) => s.auth.user)
|
||||
const canEditSensitive = hasPermission(
|
||||
currentUser,
|
||||
ADMIN_PERMISSION_RESOURCES.CHANNEL,
|
||||
ADMIN_PERMISSION_ACTIONS.SENSITIVE_WRITE
|
||||
)
|
||||
|
||||
// Data state
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@@ -148,6 +160,14 @@ export function MultiKeyManageDialog({
|
||||
|
||||
const performAction = async () => {
|
||||
if (!confirmAction || !currentRow) return
|
||||
if (
|
||||
!canEditSensitive &&
|
||||
(confirmAction.type === 'delete' ||
|
||||
confirmAction.type === 'delete-disabled')
|
||||
) {
|
||||
setConfirmAction(null)
|
||||
return
|
||||
}
|
||||
|
||||
setIsPerformingAction(true)
|
||||
try {
|
||||
@@ -331,7 +351,16 @@ export function MultiKeyManageDialog({
|
||||
<Button
|
||||
variant='destructive'
|
||||
size='sm'
|
||||
onClick={() => setConfirmAction({ type: 'delete-disabled' })}
|
||||
onClick={() => {
|
||||
if (!canEditSensitive) return
|
||||
setConfirmAction({ type: 'delete-disabled' })
|
||||
}}
|
||||
disabled={!canEditSensitive}
|
||||
title={
|
||||
canEditSensitive
|
||||
? undefined
|
||||
: t('No permission to perform this action')
|
||||
}
|
||||
>
|
||||
<Trash2 className='mr-2 h-4 w-4' />
|
||||
{t('Delete Auto-Disabled')}
|
||||
@@ -339,6 +368,11 @@ export function MultiKeyManageDialog({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!canEditSensitive && (
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('No permission to perform this action')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Table */}
|
||||
<div className='min-h-0 flex-1 overflow-auto rounded-md border'>
|
||||
@@ -392,6 +426,7 @@ export function MultiKeyManageDialog({
|
||||
<MultiKeyTableRowActions
|
||||
keyIndex={key.index}
|
||||
status={key.status}
|
||||
canDelete={canEditSensitive}
|
||||
onAction={setConfirmAction}
|
||||
/>
|
||||
),
|
||||
|
||||
+12
-1
@@ -23,12 +23,14 @@ import type { MultiKeyConfirmAction } from '../../types'
|
||||
type MultiKeyTableRowActionsProps = {
|
||||
keyIndex: number
|
||||
status: number
|
||||
canDelete: boolean
|
||||
onAction: (action: MultiKeyConfirmAction) => void
|
||||
}
|
||||
|
||||
export function MultiKeyTableRowActions({
|
||||
keyIndex,
|
||||
status,
|
||||
canDelete,
|
||||
onAction,
|
||||
}: MultiKeyTableRowActionsProps) {
|
||||
const { t } = useTranslation()
|
||||
@@ -56,7 +58,16 @@ export function MultiKeyTableRowActions({
|
||||
<Button
|
||||
variant='destructive'
|
||||
size='sm'
|
||||
onClick={() => onAction({ type: 'delete', keyIndex })}
|
||||
onClick={() => {
|
||||
if (!canDelete) return
|
||||
onAction({ type: 'delete', keyIndex })
|
||||
}}
|
||||
disabled={!canDelete}
|
||||
title={
|
||||
canDelete
|
||||
? undefined
|
||||
: t('No permission to perform this action')
|
||||
}
|
||||
>
|
||||
{t('Delete')}
|
||||
</Button>
|
||||
|
||||
+243
-82
@@ -47,7 +47,13 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
ADMIN_PERMISSION_ACTIONS,
|
||||
ADMIN_PERMISSION_RESOURCES,
|
||||
hasPermission,
|
||||
} from '@/lib/admin-permissions'
|
||||
import { getLobeIcon } from '@/lib/lobe-icon'
|
||||
import { ROLE } from '@/lib/roles'
|
||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||
import { useHiddenClickUnlock } from '@/hooks/use-hidden-click-unlock'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
@@ -104,6 +110,7 @@ import {
|
||||
SecureVerificationDialog,
|
||||
useSecureVerification,
|
||||
} from '@/features/auth/secure-verification'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import {
|
||||
fetchModels,
|
||||
getAllModels,
|
||||
@@ -198,6 +205,40 @@ const MODEL_MAPPING_PREVIEW_FALLBACK: Array<{
|
||||
|
||||
const ADVANCED_SETTINGS_EXPANDED_KEY = 'channel-advanced-settings-expanded'
|
||||
const UPSTREAM_DETECTED_MODEL_PREVIEW_LIMIT = 8
|
||||
const SENSITIVE_FORM_FIELDS = [
|
||||
'type',
|
||||
'base_url',
|
||||
'key',
|
||||
'openai_organization',
|
||||
'other',
|
||||
'key_mode',
|
||||
'param_override',
|
||||
'header_override',
|
||||
'settings',
|
||||
'setting',
|
||||
'advanced_custom',
|
||||
'is_enterprise_account',
|
||||
'vertex_key_type',
|
||||
'aws_key_type',
|
||||
'azure_responses_version',
|
||||
'force_format',
|
||||
'thinking_to_content',
|
||||
'proxy',
|
||||
'pass_through_body_enabled',
|
||||
'system_prompt',
|
||||
'system_prompt_override',
|
||||
'allow_service_tier',
|
||||
'disable_store',
|
||||
'allow_safety_identifier',
|
||||
'allow_include_obfuscation',
|
||||
'allow_inference_geo',
|
||||
'allow_speed',
|
||||
'claude_beta_query',
|
||||
'disable_task_polling_sleep',
|
||||
'upstream_model_update_check_enabled',
|
||||
'upstream_model_update_auto_sync_enabled',
|
||||
'upstream_model_update_ignored_models',
|
||||
] satisfies (keyof ChannelFormValues)[]
|
||||
|
||||
function readAdvancedSettingsPreference(): boolean {
|
||||
if (typeof window === 'undefined') return false
|
||||
@@ -280,6 +321,13 @@ export function ChannelMutateDrawer({
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const { setOpen } = useChannels()
|
||||
const currentUser = useAuthStore((s) => s.auth.user)
|
||||
const canEditSensitive = hasPermission(
|
||||
currentUser,
|
||||
ADMIN_PERMISSION_RESOURCES.CHANNEL,
|
||||
ADMIN_PERMISSION_ACTIONS.SENSITIVE_WRITE
|
||||
)
|
||||
const canRevealChannelKey = currentUser?.role === ROLE.SUPER_ADMIN
|
||||
const [fetchModelsDialogOpen, setFetchModelsDialogOpen] = useState(false)
|
||||
const [channelKey, setChannelKey] = useState<string | null>(null)
|
||||
const [isChannelKeyLoading, setIsChannelKeyLoading] = useState(false)
|
||||
@@ -307,6 +355,7 @@ export function ChannelMutateDrawer({
|
||||
|
||||
const isEditing = Boolean(currentRow)
|
||||
const channelId = currentRow?.id ?? null
|
||||
const sensitiveLocked = isEditing && !canEditSensitive
|
||||
|
||||
// Fetch channel details if editing
|
||||
const { data: channelData, isLoading: isChannelLoading } = useQuery({
|
||||
@@ -388,7 +437,7 @@ export function ChannelMutateDrawer({
|
||||
reset: resetDoubaoApiUnlock,
|
||||
} = useHiddenClickUnlock({
|
||||
requiredClicks: 10,
|
||||
disabled: currentType !== 45,
|
||||
disabled: currentType !== 45 || sensitiveLocked,
|
||||
onUnlock: () => {
|
||||
toast.info(t('Doubao custom API address editing unlocked'))
|
||||
},
|
||||
@@ -783,6 +832,11 @@ export function ChannelMutateDrawer({
|
||||
return
|
||||
}
|
||||
|
||||
if (!isEditing && !canEditSensitive) {
|
||||
toast.error(t("You don't have necessary permission"))
|
||||
return
|
||||
}
|
||||
|
||||
// For creation mode, validate key before opening dialog
|
||||
if (!isEditing) {
|
||||
const key = form.getValues('key')
|
||||
@@ -793,9 +847,12 @@ export function ChannelMutateDrawer({
|
||||
}
|
||||
|
||||
setFetchModelsDialogOpen(true)
|
||||
}, [isEditing, form, t])
|
||||
}, [isEditing, canEditSensitive, form, t])
|
||||
|
||||
const createModeFetcher = useCallback(async (): Promise<string[]> => {
|
||||
if (!canEditSensitive) {
|
||||
throw new Error(t("You don't have necessary permission"))
|
||||
}
|
||||
const response = await fetchModels({
|
||||
type: form.getValues('type'),
|
||||
key: form.getValues('key'),
|
||||
@@ -805,7 +862,7 @@ export function ChannelMutateDrawer({
|
||||
return response.data
|
||||
}
|
||||
throw new Error(response.message || 'No models fetched from upstream')
|
||||
}, [form])
|
||||
}, [canEditSensitive, form, t])
|
||||
|
||||
// Handle model operations
|
||||
const handleFillRelatedModels = useCallback(() => {
|
||||
@@ -963,6 +1020,21 @@ export function ChannelMutateDrawer({
|
||||
return
|
||||
}
|
||||
|
||||
if (sensitiveLocked) {
|
||||
const dirtyFields = form.formState.dirtyFields as Partial<
|
||||
Record<keyof ChannelFormValues, unknown>
|
||||
>
|
||||
const hasSensitiveChanges = SENSITIVE_FORM_FIELDS.some((field) =>
|
||||
Boolean(dirtyFields[field])
|
||||
)
|
||||
if (hasSensitiveChanges) {
|
||||
toast.error(
|
||||
t('You do not have permission to edit sensitive channel settings.')
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate status_code_mapping entries
|
||||
if (data.status_code_mapping?.trim()) {
|
||||
const invalidEntries = collectInvalidStatusCodeEntries(
|
||||
@@ -1038,6 +1110,7 @@ export function ChannelMutateDrawer({
|
||||
},
|
||||
[
|
||||
isEditing,
|
||||
sensitiveLocked,
|
||||
form,
|
||||
confirmMissingModelMappings,
|
||||
confirmStatusCodeRisk,
|
||||
@@ -1105,6 +1178,17 @@ export function ChannelMutateDrawer({
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{sensitiveLocked && (
|
||||
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
|
||||
<AlertDescription>
|
||||
{t('Sensitive channel settings are read-only for your account.')}{' '}
|
||||
{t(
|
||||
'You can still edit non-sensitive operations fields such as models, groups, priority, and weight.'
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id='channel-form'
|
||||
@@ -1135,78 +1219,103 @@ export function ChannelMutateDrawer({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='type'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Type *')}</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
options={channelTypeOptions}
|
||||
value={String(field.value)}
|
||||
onValueChange={(value) => {
|
||||
const nextType = Number(value)
|
||||
if (
|
||||
Number.isInteger(nextType) &&
|
||||
nextType > 0
|
||||
) {
|
||||
field.onChange(nextType)
|
||||
}
|
||||
}}
|
||||
placeholder={t('Select channel type')}
|
||||
searchPlaceholder={t('Search channel type...')}
|
||||
emptyText={t('No channel type found.')}
|
||||
allowCustomValue
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<fieldset
|
||||
disabled={sensitiveLocked}
|
||||
className='min-w-0 disabled:opacity-60'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='type'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Type *')}</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
options={channelTypeOptions}
|
||||
value={String(field.value)}
|
||||
onValueChange={(value) => {
|
||||
const nextType = Number(value)
|
||||
if (
|
||||
Number.isInteger(nextType) &&
|
||||
nextType > 0
|
||||
) {
|
||||
field.onChange(nextType)
|
||||
}
|
||||
}}
|
||||
placeholder={t('Select channel type')}
|
||||
searchPlaceholder={t(
|
||||
'Search channel type...'
|
||||
)}
|
||||
emptyText={t('No channel type found.')}
|
||||
allowCustomValue
|
||||
/>
|
||||
</FormControl>
|
||||
{sensitiveLocked && (
|
||||
<FormDescription>
|
||||
{t(
|
||||
'No permission to perform this action'
|
||||
)}
|
||||
</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='status'
|
||||
render={({ field }) => (
|
||||
<FormItem className={sideDrawerSwitchItemClassName()}>
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<FormLabel>{t('Enabled')}</FormLabel>
|
||||
<FormDescription className='text-xs'>
|
||||
{t('Enable or disable this channel')}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value === 1}
|
||||
onCheckedChange={(checked) =>
|
||||
field.onChange(checked ? 1 : 2)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{currentType === 1 && (
|
||||
{!isEditing && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='openai_organization'
|
||||
name='status'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('OpenAI Organization')}</FormLabel>
|
||||
<FormItem className={sideDrawerSwitchItemClassName()}>
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<FormLabel>{t('Enabled')}</FormLabel>
|
||||
<FormDescription className='text-xs'>
|
||||
{t('Enable or disable this channel')}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input placeholder={t('org-...')} {...field} />
|
||||
<Switch
|
||||
checked={field.value === 1}
|
||||
onCheckedChange={(checked) =>
|
||||
field.onChange(checked ? 1 : 2)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(FIELD_DESCRIPTIONS.OPENAI_ORG)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentType === 1 && (
|
||||
<fieldset
|
||||
disabled={sensitiveLocked}
|
||||
className='disabled:opacity-60'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='openai_organization'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('OpenAI Organization')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('org-...')} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{sensitiveLocked
|
||||
? t(
|
||||
'No permission to perform this action'
|
||||
)
|
||||
: t(FIELD_DESCRIPTIONS.OPENAI_ORG)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
</ChannelBasicSection>
|
||||
|
||||
{/* ── API Access ── */}
|
||||
@@ -1219,6 +1328,20 @@ export function ChannelMutateDrawer({
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{sensitiveLocked && (
|
||||
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
|
||||
<AlertDescription>
|
||||
{t(
|
||||
'No permission to perform this action'
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<fieldset
|
||||
disabled={sensitiveLocked}
|
||||
className='space-y-4 disabled:opacity-60'
|
||||
>
|
||||
{/* Azure (type 3) */}
|
||||
{currentType === 3 && (
|
||||
<>
|
||||
@@ -2004,7 +2127,7 @@ export function ChannelMutateDrawer({
|
||||
)}
|
||||
</div>
|
||||
</FormDescription>
|
||||
{isEditing && (
|
||||
{isEditing && canRevealChannelKey && (
|
||||
<div className='border-border/60 mt-4 flex flex-col gap-3 border-y border-dashed py-4'>
|
||||
<div className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div>
|
||||
@@ -2081,7 +2204,10 @@ export function ChannelMutateDrawer({
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleRefreshCodexCredential}
|
||||
disabled={isCodexCredentialRefreshing}
|
||||
disabled={
|
||||
sensitiveLocked ||
|
||||
isCodexCredentialRefreshing
|
||||
}
|
||||
>
|
||||
{isCodexCredentialRefreshing ? (
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
@@ -2207,6 +2333,7 @@ export function ChannelMutateDrawer({
|
||||
/>
|
||||
)}
|
||||
</ChannelAuthSection>
|
||||
</fieldset>
|
||||
</ChannelApiAccessSection>
|
||||
|
||||
{/* ── Models & Groups ── */}
|
||||
@@ -2324,18 +2451,28 @@ export function ChannelMutateDrawer({
|
||||
{t('Fill All Models')}
|
||||
</Button>
|
||||
{MODEL_FETCHABLE_TYPES.has(currentType) && (
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleFetchModels}
|
||||
>
|
||||
<Sparkles
|
||||
className='mr-2 h-4 w-4'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
{t('Fetch from Upstream')}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={handleFetchModels}
|
||||
disabled={!isEditing && !canEditSensitive}
|
||||
>
|
||||
<Sparkles
|
||||
className='mr-2 h-4 w-4'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
{t('Fetch from Upstream')}
|
||||
</Button>
|
||||
{!isEditing && !canEditSensitive && (
|
||||
<span className='text-muted-foreground basis-full text-xs'>
|
||||
{t(
|
||||
'No permission to perform this action'
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
type='button'
|
||||
@@ -2752,6 +2889,15 @@ export function ChannelMutateDrawer({
|
||||
)}
|
||||
/>
|
||||
|
||||
{sensitiveLocked && (
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('No permission to perform this action')}
|
||||
</p>
|
||||
)}
|
||||
<fieldset
|
||||
disabled={sensitiveLocked}
|
||||
className='space-y-4 disabled:opacity-60'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='param_override'
|
||||
@@ -2827,7 +2973,7 @@ export function ChannelMutateDrawer({
|
||||
<Textarea
|
||||
value={field.value || ''}
|
||||
onChange={field.onChange}
|
||||
disabled={isSubmitting}
|
||||
disabled={sensitiveLocked || isSubmitting}
|
||||
rows={8}
|
||||
placeholder={t(
|
||||
'Override request parameters. Cannot override stream parameter.'
|
||||
@@ -2923,7 +3069,7 @@ export function ChannelMutateDrawer({
|
||||
rows={6}
|
||||
value={field.value || ''}
|
||||
onChange={field.onChange}
|
||||
disabled={isSubmitting}
|
||||
disabled={sensitiveLocked || isSubmitting}
|
||||
placeholder={t(
|
||||
'Enter JSON to override request headers'
|
||||
)}
|
||||
@@ -2944,6 +3090,7 @@ export function ChannelMutateDrawer({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2953,6 +3100,19 @@ export function ChannelMutateDrawer({
|
||||
title={t('Channel Extra Settings')}
|
||||
icon={<Settings className='h-4 w-4' />}
|
||||
/>
|
||||
{sensitiveLocked && (
|
||||
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
|
||||
<AlertDescription>
|
||||
{t(
|
||||
'No permission to perform this action'
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<fieldset
|
||||
disabled={sensitiveLocked}
|
||||
className='space-y-4 disabled:opacity-60'
|
||||
>
|
||||
{(currentType === 1 || currentType === 14) && (
|
||||
<div className='border-border/60 flex flex-col gap-3 border-y py-4'>
|
||||
<SubHeading
|
||||
@@ -3468,6 +3628,7 @@ export function ChannelMutateDrawer({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
</div>
|
||||
</ChannelAdvancedSection>
|
||||
</>
|
||||
@@ -3491,7 +3652,7 @@ export function ChannelMutateDrawer({
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
{paramOverrideEditorOpen && (
|
||||
{paramOverrideEditorOpen && !sensitiveLocked && (
|
||||
<ParamOverrideEditorDialog
|
||||
open={paramOverrideEditorOpen}
|
||||
value={form.watch('param_override') || ''}
|
||||
@@ -3505,7 +3666,7 @@ export function ChannelMutateDrawer({
|
||||
/>
|
||||
)}
|
||||
|
||||
{advancedCustomEditorOpen && (
|
||||
{advancedCustomEditorOpen && !sensitiveLocked && (
|
||||
<AdvancedCustomEditorDialog
|
||||
open={advancedCustomEditorOpen}
|
||||
value={form.watch('advanced_custom') || ''}
|
||||
|
||||
Reference in New Issue
Block a user