RSS Git Download  Clone
Raw Blame History 7kB 204 lines
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useDisplay } from 'vuetify'
import { useI18nStore } from '../stores/i18n'
import P3xrDialog from '../components/P3xrDialog.vue'

const props = defineProps<{
    open: boolean
    username: string
    rules: string
    isNew: boolean
}>()

const emit = defineEmits<{
    close: [result?: { username: string; rules: string[] }]
}>()

const i18n = useI18nStore()
const strings = computed(() => i18n.strings)
const { width } = useDisplay()
const isWide = computed(() => width.value >= 600)

const localUsername = ref('')
const enabled = ref(true)
const nopass = ref(false)
const password = ref('')
const commandsList = ref<string[]>([])
const keysList = ref<string[]>([])
const channelsList = ref<string[]>([])

function parseRules(rules: string) {
    const tokens = rules.trim().split(/\s+/).filter(Boolean)
    enabled.value = true
    nopass.value = false
    password.value = ''
    const cmds: string[] = [], keys: string[] = [], channels: string[] = []

    for (const t of tokens) {
        if (t === 'on') enabled.value = true
        else if (t === 'off') enabled.value = false
        else if (t === 'nopass') nopass.value = 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)
    }

    commandsList.value = cmds
    keysList.value = keys
    channelsList.value = channels
}

watch(() => props.open, (v) => {
    if (v) {
        localUsername.value = props.username
        parseRules(props.rules)
    }
})

function handleSave() {
    const u = localUsername.value.trim()
    if (!u) return
    const rules: string[] = [enabled.value ? 'on' : 'off']
    if (!props.isNew) {
        // Reset permissions first so removals take effect
        rules.push('nocommands', 'resetkeys', 'resetchannels')
        if (nopass.value) rules.push('resetpass', 'nopass')
        else if (password.value.trim()) rules.push('resetpass', '>' + password.value.trim())
    } else {
        if (nopass.value) rules.push('nopass')
        else if (password.value.trim()) rules.push('>' + password.value.trim())
    }
    rules.push(...commandsList.value, ...keysList.value, ...channelsList.value)
    emit('close', { username: u, rules })
}

function handleCancel() {
    emit('close')
}

function chipColor(rule: any): string {
    const val = typeof rule === 'string' ? rule : rule?.value ?? rule?.raw ?? ''
    if (val.startsWith('-')) return 'error'
    if (val.startsWith('+')) return 'primary'
    return 'primary'
}
</script>

<template>
    <P3xrDialog
        v-if="open"
        :open="true"
        :title="isNew ? (strings?.page?.acl?.createUser || 'Create User') : (strings?.page?.acl?.editUser || 'Edit User')"
        width="600px"
        @close="handleCancel"
    >
        <v-text-field
            v-model="localUsername"
            :label="strings?.page?.acl?.username || 'Username'"
            :disabled="!isNew"
            variant="outlined"
            density="comfortable"
            hide-details
            class="mb-3"
        />

        <v-switch
            v-model="enabled"
            :label="strings?.page?.acl?.enabled || 'Enabled'"
            density="comfortable"
            hide-details
            class="mb-2"
        />

        <v-checkbox
            v-model="nopass"
            :label="strings?.page?.acl?.noPassword || 'No password (nopass)'"
            density="comfortable"
            hide-details
            class="mb-2"
        />

        <v-text-field
            v-if="!nopass"
            v-model="password"
            :label="strings?.page?.acl?.password || 'Password'"
            type="password"
            autocomplete="new-password"
            variant="outlined"
            density="comfortable"
            :hint="!isNew ? (strings?.page?.acl?.passwordHint || 'Leave empty to keep current password') : undefined"
            :persistent-hint="!isNew"
            :hide-details="isNew"
            class="mb-3"
        />

        <v-combobox
            v-model="commandsList"
            :label="strings?.page?.acl?.commands || 'Commands'"
            chips
            multiple
            closable-chips
            variant="outlined"
            density="comfortable"
            :hint="strings?.page?.acl?.commandsHint || 'e.g., +@all or +@read -@dangerous'"
            persistent-hint
            :placeholder="commandsList.length === 0 ? '+@all, -@dangerous ...' : ''"
            class="mb-3"
        >
            <template #chip="{ props: chipProps, item }">
                <v-chip v-bind="chipProps" :color="chipColor(item)" variant="tonal" size="small" />
            </template>
        </v-combobox>

        <v-combobox
            v-model="keysList"
            :label="strings?.page?.acl?.keys || 'Key Patterns'"
            chips
            multiple
            closable-chips
            variant="outlined"
            density="comfortable"
            :hint="strings?.page?.acl?.keysHint || 'e.g., ~* or ~user:*'"
            persistent-hint
            :placeholder="keysList.length === 0 ? '~*, ~user:* ...' : ''"
            class="mb-3"
        >
            <template #chip="{ props: chipProps }">
                <v-chip v-bind="chipProps" color="primary" variant="tonal" size="small" />
            </template>
        </v-combobox>

        <v-combobox
            v-model="channelsList"
            :label="strings?.page?.acl?.channels || 'Pub/Sub Channels'"
            chips
            multiple
            closable-chips
            variant="outlined"
            density="comfortable"
            :hint="strings?.page?.acl?.channelsHint || 'e.g., &* or &notifications:*'"
            persistent-hint
            :placeholder="channelsList.length === 0 ? '&*, &notifications:* ...' : ''"
        >
            <template #chip="{ props: chipProps }">
                <v-chip v-bind="chipProps" color="primary" variant="tonal" size="small" />
            </template>
        </v-combobox>

        <template #actions>
            <v-btn variant="flat" color="warning" @click="handleCancel">
                <v-icon :class="{ 'mr-1': isWide }">mdi-close-circle</v-icon>
                <span v-if="isWide">{{ strings?.intention?.cancel || 'Cancel' }}</span>
                <v-tooltip v-if="!isWide" activator="parent" location="top">{{ strings?.intention?.cancel || 'Cancel' }}</v-tooltip>
            </v-btn>
            <v-btn variant="flat" color="primary" @click="handleSave" :disabled="!localUsername.trim()">
                <v-icon :class="{ 'mr-1': isWide }">mdi-check</v-icon>
                <span v-if="isWide">{{ strings?.intention?.save || 'Save' }}</span>
                <v-tooltip v-if="!isWide" activator="parent" location="top">{{ strings?.intention?.save || 'Save' }}</v-tooltip>
            </v-btn>
        </template>
    </P3xrDialog>
</template>