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:
Calcium-Ion
2026-06-27 17:01:59 +08:00
committed by GitHub
parent 6c35e1ef26
commit 4aee5f7d5a
52 changed files with 2778 additions and 255 deletions
@@ -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)
}}
@@ -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}
/>
),
@@ -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>
@@ -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') || ''}