This commit is contained in:
2026-08-02 11:09:50 +02:00
parent 659fd0f304
commit aa486be6e5
8 changed files with 221 additions and 60 deletions
@@ -4,10 +4,11 @@ import {useUserStore} from '@/stores/user'
import {useApi} from '@/composables/useApi'
import {usePermissions} from '@/composables/usePermissions'
import ChannelPermissionEditor from './ChannelPermissionEditor.vue'
import {toChannelPermissionMask, type ChannelPermissionMask} from '@/types/permissions'
import {toChannelPermissionMask, type ChannelPermissionMask, type ChannelPermissions} from '@/types/permissions'
interface Role { id: string; server_id: string; name: string; is_default: boolean }
interface Channel { id: string; name?: string | null }
type TargetType = 'role' | 'user'
const props = defineProps<{ modelValue: boolean; channel: Channel | null; serverId: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
@@ -16,57 +17,68 @@ const users = useUserStore()
const api = useApi()
const permissionsApi = usePermissions()
const roles = ref<Role[]>([])
const targetType = ref<'role' | 'user'>('role')
const configured = ref<ChannelPermissions>({users: [], roles: []})
const targetType = ref<TargetType>('role')
const targetId = ref<string | null>(null)
const addTargetId = ref<string | null>(null)
const mask = ref<ChannelPermissionMask>(toChannelPermissionMask(0))
const loadingTargets = ref(false)
const loadingPermission = ref(false)
const loading = ref(false)
const saving = ref(false)
const error = ref<string | null>(null)
const targets = computed(() => targetType.value === 'role' ? roles.value : users.users)
const targetLabel = computed(() => targetType.value === 'role' ? 'Rôle' : 'Membre')
const targetName = computed(() => {
const target = targets.value.find(item => item.id === targetId.value)
return targetType.value === 'role' ? (target as Role | undefined)?.name : (target as typeof users.users[number] | undefined)?.username
const configuredEntries = computed(() => targetType.value === 'role' ? configured.value.roles : configured.value.users)
type PermissionEntry = ChannelPermissions['roles'][number] | ChannelPermissions['users'][number]
function entryTargetId(entry: PermissionEntry): string {
return 'role_id' in entry ? entry.role_id : entry.user_id
}
function entryName(entry: PermissionEntry): string {
const id = entryTargetId(entry)
return targetType.value === 'role'
? roles.value.find(role => role.id === id)?.name || id
: users.users.find(user => user.id === id)?.username || id
}
const availableTargets = computed(() => {
const used = new Set(configuredEntries.value.map(entryTargetId))
return targetType.value === 'role'
? roles.value.filter(role => !used.has(role.id))
: users.users.filter(user => !used.has(user.id))
})
const selectedName = computed(() => {
if (!targetId.value) return ''
if (targetType.value === 'role') return roles.value.find(role => role.id === targetId.value)?.name || targetId.value
return users.users.find(user => user.id === targetId.value)?.username || targetId.value
})
watch(() => props.modelValue, async open => {
if (open) await loadTargets()
if (open) await load()
else reset()
})
watch(targetType, () => { targetId.value = null; mask.value = toChannelPermissionMask(0); error.value = null })
watch(targetId, loadPermission)
watch(targetType, () => { targetId.value = null; addTargetId.value = null; mask.value = toChannelPermissionMask(0); error.value = null })
watch(addTargetId, id => {
if (!id) return
targetId.value = id
mask.value = toChannelPermissionMask(0)
addTargetId.value = null
})
async function loadTargets() {
loadingTargets.value = true
error.value = null
async function load() {
if (!props.channel) return
loading.value = true; error.value = null
try {
const [roleResponse] = await Promise.all([
api.get('/groups'),
users.fetchUsers(props.serverId),
])
const [roleResponse] = await Promise.all([api.get('/groups'), users.fetchUsers(props.serverId)])
if (!roleResponse.ok) throw new Error('Impossible de charger les rôles')
roles.value = (await roleResponse.json() as Role[]).filter(role => role.server_id === props.serverId)
configured.value = await permissionsApi.getChannelPermissions(props.channel.id)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Erreur lors du chargement des cibles'
} finally { loadingTargets.value = false }
error.value = e instanceof Error ? e.message : 'Erreur lors du chargement des permissions'
} finally { loading.value = false }
}
async function loadPermission() {
if (!props.channel || !targetId.value) return
loadingPermission.value = true
function selectExisting(id: string) {
targetId.value = id
const entry = configuredEntries.value.find(item => entryTargetId(item) === id)
mask.value = entry?.permissions ?? toChannelPermissionMask(0)
error.value = null
try {
const permission = targetType.value === 'role'
? await permissionsApi.getChannelRolePermission(props.channel.id, targetId.value)
: await permissionsApi.getChannelUserPermission(props.channel.id, targetId.value)
mask.value = permission.permissions
} catch (e) {
// L'absence de ligne signifie un masque vide, pas une erreur d'interface.
if (e instanceof Error && ((e as Error & { status?: number }).status === 404 || /404|introuvable/i.test(e.message))) mask.value = toChannelPermissionMask(0)
else error.value = e instanceof Error ? e.message : 'Erreur lors du chargement des permissions'
} finally { loadingPermission.value = false }
}
async function save(value: ChannelPermissionMask) {
@@ -75,7 +87,8 @@ async function save(value: ChannelPermissionMask) {
try {
if (targetType.value === 'role') await permissionsApi.setChannelRolePermission(props.channel.id, targetId.value, value)
else await permissionsApi.setChannelUserPermission(props.channel.id, targetId.value, value)
close()
configured.value = await permissionsApi.getChannelPermissions(props.channel.id)
mask.value = value
} catch (e) { error.value = e instanceof Error ? e.message : 'Erreur lors de la sauvegarde' }
finally { saving.value = false }
}
@@ -86,14 +99,17 @@ async function resetPermission() {
try {
if (targetType.value === 'role') await permissionsApi.removeChannelRolePermission(props.channel.id, targetId.value)
else await permissionsApi.removeChannelUserPermission(props.channel.id, targetId.value)
configured.value = await permissionsApi.getChannelPermissions(props.channel.id)
targetId.value = null
mask.value = toChannelPermissionMask(0)
} catch (e) {
if (!(e instanceof Error && ((e as Error & { status?: number }).status === 404 || /404|introuvable/i.test(e.message)))) error.value = e instanceof Error ? e.message : 'Erreur lors de la réinitialisation'
const status = e instanceof Error ? (e as Error & {status?: number}).status : undefined
if (status !== 404) error.value = e instanceof Error ? e.message : 'Erreur lors de la réinitialisation'
} finally { saving.value = false }
}
function close() { emit('update:modelValue', false) }
function reset() { targetId.value = null; mask.value = toChannelPermissionMask(0); error.value = null }
function reset() { configured.value = {users: [], roles: []}; targetId.value = null; addTargetId.value = null; mask.value = toChannelPermissionMask(0); error.value = null }
</script>
<template>
@@ -106,28 +122,84 @@ function reset() { targetId.value = null; mask.value = toChannelPermissionMask(0
<v-tab value="role">Rôles</v-tab>
<v-tab value="user">Membres</v-tab>
</v-tabs>
<v-select
v-model="targetId"
:items="targets"
:item-title="targetType === 'role' ? 'name' : 'username'"
item-value="id"
:label="targetLabel"
:loading="loadingTargets"
clearable
/>
<v-chip v-if="targetName" class="mb-3" color="primary" size="small">{{ targetName }}</v-chip>
<ChannelPermissionEditor
v-if="targetId"
v-model="mask"
:loading="loadingPermission || saving"
title="Permissions du canal"
@save="save"
/>
<v-btn v-if="targetId" class="mt-3" color="error" variant="text" :loading="saving" @click="resetPermission">
Réinitialiser les permissions directes
</v-btn>
<v-row class="permission-layout" dense>
<v-col cols="12" md="4" class="permission-sidebar">
<div class="text-subtitle-2 mb-2">Cibles configurées</div>
<v-list v-if="!loading && configuredEntries.length" density="compact" lines="one" border class="permission-list">
<v-list-item
v-for="entry in configuredEntries"
:key="entry.id"
:active="targetId === entryTargetId(entry)"
@click="selectExisting(entryTargetId(entry))"
>
<v-list-item-title>{{ entryName(entry) }}</v-list-item-title>
<template #append><v-chip size="x-small" variant="tonal">{{ entry.permissions.toString(2).replace(/^0+/, '').length || 0 }}</v-chip></template>
</v-list-item>
</v-list>
<div v-else-if="!loading" class="text-medium-emphasis text-body-2 py-4">Aucune permission directe configurée.</div>
<v-skeleton-loader v-else type="list-item-two-line" />
<v-select
v-model="addTargetId"
:items="availableTargets"
:item-title="targetType === 'role' ? 'name' : 'username'"
item-value="id"
:label="targetType === 'role' ? 'Ajouter un rôle' : 'Ajouter un membre'"
:loading="loading"
clearable
class="mt-3"
hide-details
/>
</v-col>
<v-col cols="12" md="8">
<div v-if="targetId" class="permission-editor">
<v-chip class="mb-3" color="primary" size="small">{{ selectedName }}</v-chip>
<ChannelPermissionEditor
v-model="mask"
:loading="saving"
title="Permissions du canal"
@save="save"
/>
<v-btn class="mt-3" color="error" variant="text" :loading="saving" @click="resetPermission">Réinitialiser les permissions directes</v-btn>
</div>
<v-sheet v-else class="empty-editor d-flex align-center justify-center text-center text-medium-emphasis" rounded border>
Sélectionnez une cible à gauche ou ajoutez-en une pour modifier ses permissions.
</v-sheet>
</v-col>
</v-row>
</v-card-text>
<v-card-actions><v-spacer/><v-btn variant="text" @click="close">Fermer</v-btn></v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
.permission-layout {
min-height: 390px;
}
.permission-sidebar {
border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
.permission-list {
max-height: 280px;
overflow-y: auto;
}
.empty-editor {
min-height: 360px;
padding: 2rem;
}
@media (max-width: 959px) {
.permission-sidebar {
border-right: 0;
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
padding-bottom: 1rem;
}
.empty-editor {
min-height: 180px;
}
}
</style>
@@ -1,6 +1,9 @@
import {useApi} from '@/composables/useApi'
import {
type ChannelRolePermission,
type ChannelPermissions,
type ChannelPermissionsDto,
channelPermissionsFromDto,
type ChannelRolePermissionDto,
channelRolePermissionFromDto,
type ChannelUserPermission,
@@ -130,6 +133,12 @@ export function usePermissions() {
// Permissions canal - utilisateur
// ---------------------------------------------------------------------------
async function getChannelPermissions(channelId: string): Promise<ChannelPermissions> {
const response = await api.get(`/channels/${channelId}/permissions`)
const dto = await parseResponse<ChannelPermissionsDto>(response)
return channelPermissionsFromDto(dto)
}
async function getChannelUserPermission(
channelId: string,
userId: string,
@@ -217,6 +226,7 @@ export function usePermissions() {
}
return {
getChannelPermissions,
getServerUserPermission,
setServerUserPermission,
removeServerUserPermission,
+20 -1
View File
@@ -52,6 +52,11 @@ export interface ChannelRolePermission {
permissions: ChannelPermissionMask
}
export interface ChannelPermissions {
users: ChannelUserPermission[]
roles: ChannelRolePermission[]
}
export interface ServerUserPermissionDto {
id: string
server_id: string
@@ -80,6 +85,11 @@ export interface ChannelRolePermissionDto {
permissions: number | string
}
export interface ChannelPermissionsDto {
users: ChannelUserPermissionDto[]
roles: ChannelRolePermissionDto[]
}
function asServerPermissionMask(value: bigint): ServerPermissionMask {
return value as ServerPermissionMask
}
@@ -442,4 +452,13 @@ export function channelRolePermissionFromDto(
role_id: dto.role_id,
permissions: toChannelPermissionMask(dto.permissions),
}
}
}
export function channelPermissionsFromDto(
dto: ChannelPermissionsDto,
): ChannelPermissions {
return {
users: dto.users.map(channelUserPermissionFromDto),
roles: dto.roles.map(channelRolePermissionFromDto),
}
}