@@ -392,6 +426,7 @@ export function MultiKeyManageDialog({
),
diff --git a/web/default/src/features/channels/components/dialogs/multi-key-table-row-actions.tsx b/web/default/src/features/channels/components/dialogs/multi-key-table-row-actions.tsx
index deece337..08345f96 100644
--- a/web/default/src/features/channels/components/dialogs/multi-key-table-row-actions.tsx
+++ b/web/default/src/features/channels/components/dialogs/multi-key-table-row-actions.tsx
@@ -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({
diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
index cab16261..380d10e5 100644
--- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
+++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
@@ -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
(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 => {
+ 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
+ >
+ 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({
+ {sensitiveLocked && (
+
+
+ {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.'
+ )}
+
+
+ )}
+
- (
-
- {t('Type *')}
-
- {
- 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
- />
-
-
-
- )}
- />
+
-