import { useState, useEffect } from 'react' import { Button, TextField, Switch, Checkbox, FormControlLabel, useMediaQuery, Tooltip, Box, Chip, Autocomplete, useTheme, Alert } from '@mui/material' import { Done, Cancel } from '@mui/icons-material' import { useI18nStore } from '../stores/i18n.store' import { useCommonStore } from '../stores/common.store' import P3xrDialog from '../components/P3xrDialog' interface AclUserDialogProps { open: boolean onClose: (result?: { username: string; rules: string[] }) => void username?: string rules?: string isNew: boolean } function parseRules(rules: string) { const tokens = rules.trim().split(/\s+/).filter(Boolean) let enabled = true, nopass = false const cmds: string[] = [], keys: string[] = [], channels: string[] = [] for (const t of tokens) { if (t === 'on') enabled = true else if (t === 'off') enabled = false else if (t === 'nopass') nopass = true else if (t.startsWith('>') || t.startsWith('<') || t.startsWith('#') || t === 'resetpass' || t === 'sanitize-payload' || t === 'skip-sanitize-payload') continue else if (t.startsWith('+') || t.startsWith('-') || t === 'allcommands' || t === 'nocommands') cmds.push(t) else if (t.startsWith('~') || t.startsWith('%') || t === 'allkeys' || t === 'resetkeys') keys.push(t) else if (t.startsWith('&') || t === 'allchannels' || t === 'resetchannels') channels.push(t) } return { enabled, nopass, cmds, keys, channels } } function chipColor(rule: string): 'primary' | 'error' { return rule.startsWith('-') ? 'error' : 'primary' } interface AclOption { label: string; groupKey: string } const CMD_DEFS: AclOption[] = [ '+@all', '-@all', '+@read', '-@read', '+@write', '-@write', '+@admin', '-@admin', '+@dangerous', '-@dangerous', ].map(label => ({ label, groupKey: 'groupCommon' })).concat([ '+@string', '+@hash', '+@list', '+@set', '+@sortedset', '+@stream', '+@geo', '+@bitmap', '+@hyperloglog', ].map(label => ({ label, groupKey: 'groupDataTypes' }))).concat([ '+@keyspace', '+@pubsub', '+@connection', '+@transaction', '+@scripting', '+@fast', '+@slow', '+@blocking', ].map(label => ({ label, groupKey: 'groupOperations' }))) const KEY_OPTIONS = ['~*', '%R~*', '%W~*', 'resetkeys'] const CHANNEL_OPTIONS = ['&*', 'resetchannels'] export default function AclUserDialog({ open, onClose, username: initUsername = '', rules: initRules = '', isNew }: AclUserDialogProps) { const strings = useI18nStore(s => s.strings) const confirm = useCommonStore(s => s.confirm) const isWide = useMediaQuery('(min-width: 600px)') const muiTheme = useTheme() const [username, setUsername] = useState('') const [enabled, setEnabled] = useState(true) const [nopass, setNopass] = useState(false) const [password, setPassword] = useState('') const [commandsList, setCommandsList] = useState([]) const [keysList, setKeysList] = useState([]) const [channelsList, setChannelsList] = useState([]) useEffect(() => { if (open) { setUsername(initUsername) setPassword('') const parsed = parseRules(initRules) setEnabled(parsed.enabled) setNopass(parsed.nopass) setCommandsList(parsed.cmds) setKeysList(parsed.keys) setChannelsList(parsed.channels) } }, [open, initUsername, initRules]) if (!open) return null const handleSave = async () => { const u = username.trim() if (!u) return try { await confirm({ message: strings?.intention?.areYouSure }) } catch { return } const rules: string[] = [enabled ? 'on' : 'off'] if (!isNew) { // Reset permissions first so removals take effect rules.push('nocommands', 'resetkeys', 'resetchannels') if (nopass) rules.push('resetpass', 'nopass') else if (password.trim()) rules.push('resetpass', '>' + password.trim()) } else { if (nopass) rules.push('nopass') else if (password.trim()) rules.push('>' + password.trim()) } rules.push(...commandsList, ...keysList, ...channelsList) onClose({ username: u, rules }) } const handleCancel = () => onClose() const title = isNew ? strings?.page?.acl?.createUser : strings?.page?.acl?.editUser const primaryBg = muiTheme.palette.primary.main const primaryFg = muiTheme.palette.primary.contrastText const warnBg = muiTheme.palette.warning.main const warnFg = muiTheme.palette.warning.contrastText const chipInput = (label: string, hint: string, placeholder: string, value: string[], onChange: (v: string[]) => void, options: string[]) => ( !value.includes(o))} value={value} onChange={(_, newValue) => onChange(newValue as string[])} renderValue={(val, getItemProps) => (val as string[]).map((option, index) => { const { key, ...rest } = getItemProps({ index }) return }) } renderInput={(params) => ( )} /> ) return ( {isWide ? ( ) : ( )} {isWide ? ( ) : ( )} } > setUsername(e.target.value)} disabled={!isNew} /> {username === 'default' && ( {strings?.page?.acl?.defaultUserWarning} )} setEnabled(v)} />} label={strings?.page?.acl?.enabled} /> setNopass(v)} />} label={strings?.page?.acl?.noPassword} /> {!nopass && ( setPassword(e.target.value)} helperText={!isNew ? strings?.page?.acl?.passwordHint : undefined} /> )} !commandsList.includes(o.label))} groupBy={(o) => typeof o === 'string' ? '' : (strings?.page?.acl as any)?.[o.groupKey] || o.groupKey} getOptionLabel={(o) => typeof o === 'string' ? o : o.label} value={commandsList} onChange={(_, v) => setCommandsList(v.map((x: any) => typeof x === 'string' ? x : x.label))} renderValue={(val, getItemProps) => (val as string[]).map((option, index) => { const { key, ...rest } = getItemProps({ index }) const s = String(option) const deny = s.charAt(0) === '-' return }) } renderInput={(params) => ( )} /> {chipInput( strings?.page?.acl?.keys, strings?.page?.acl?.keysHint, '~*, ~user:* ...', keysList, setKeysList, KEY_OPTIONS )} {chipInput( strings?.page?.acl?.channels, strings?.page?.acl?.channelsHint, '&*, ¬ifications:* ...', channelsList, setChannelsList, CHANNEL_OPTIONS )} ) }