This commit is contained in:
2026-08-02 10:34:18 +02:00
parent 9dbb7ffd5b
commit 659fd0f304
4 changed files with 163 additions and 6 deletions
@@ -0,0 +1,133 @@
<script lang="ts" setup>
import {computed, ref, watch} from 'vue'
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'
interface Role { id: string; server_id: string; name: string; is_default: boolean }
interface Channel { id: string; name?: string | null }
const props = defineProps<{ modelValue: boolean; channel: Channel | null; serverId: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
const users = useUserStore()
const api = useApi()
const permissionsApi = usePermissions()
const roles = ref<Role[]>([])
const targetType = ref<'role' | 'user'>('role')
const targetId = ref<string | null>(null)
const mask = ref<ChannelPermissionMask>(toChannelPermissionMask(0))
const loadingTargets = ref(false)
const loadingPermission = 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
})
watch(() => props.modelValue, async open => {
if (open) await loadTargets()
else reset()
})
watch(targetType, () => { targetId.value = null; mask.value = toChannelPermissionMask(0); error.value = null })
watch(targetId, loadPermission)
async function loadTargets() {
loadingTargets.value = true
error.value = null
try {
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)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Erreur lors du chargement des cibles'
} finally { loadingTargets.value = false }
}
async function loadPermission() {
if (!props.channel || !targetId.value) return
loadingPermission.value = true
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) {
if (!props.channel || !targetId.value) return
saving.value = true; error.value = null
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()
} catch (e) { error.value = e instanceof Error ? e.message : 'Erreur lors de la sauvegarde' }
finally { saving.value = false }
}
async function resetPermission() {
if (!props.channel || !targetId.value) return
saving.value = true; error.value = null
try {
if (targetType.value === 'role') await permissionsApi.removeChannelRolePermission(props.channel.id, targetId.value)
else await permissionsApi.removeChannelUserPermission(props.channel.id, targetId.value)
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'
} finally { saving.value = false }
}
function close() { emit('update:modelValue', false) }
function reset() { targetId.value = null; mask.value = toChannelPermissionMask(0); error.value = null }
</script>
<template>
<v-dialog :model-value="modelValue" max-width="760" @update:model-value="emit('update:modelValue', $event)">
<v-card>
<v-card-title>Permissions {{ channel?.name || 'Canal' }}</v-card-title>
<v-card-text>
<v-alert v-if="error" class="mb-4" type="error" density="compact">{{ error }}</v-alert>
<v-tabs v-model="targetType" class="mb-4">
<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-card-text>
<v-card-actions><v-spacer/><v-btn variant="text" @click="close">Fermer</v-btn></v-card-actions>
</v-card>
</v-dialog>
</template>
+4 -3
View File
@@ -22,10 +22,11 @@ export function usePermissions() {
async function parseResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const error = await response.json().catch(() => null)
throw new Error(
const exception = new Error(
error?.message || 'Erreur lors de la gestion des permissions',
)
) as Error & { status?: number }
exception.status = response.status
throw exception
}
if (response.status === 204) {
+19
View File
@@ -9,6 +9,8 @@ import CreateCategoryDialog from '@/components/category/CreateCategoryDialog.vue
import {type MenuItem, useContextMenu} from '@/composables/useContextMenu'
import {useUserStore} from "@/stores/user.ts";
import {useServerStore} from "@/stores/server.ts";
import ChannelPermissionsDialog from '@/components/permissions/ChannelPermissionsDialog.vue'
import {useAuthStore} from '@/stores/auth'
const props = defineProps<{
serverId: string
@@ -22,6 +24,9 @@ const userStore = useUserStore()
const serverStore = useServerStore()
const {currentTree} = storeToRefs(serverStore)
const {openContextMenu} = useContextMenu()
const authStore = useAuthStore()
const showPermissionsDialog = ref(false)
const selectedChannel = ref<any | null>(null)
const loadServerData = async (targetServerId: string) => {
@@ -111,6 +116,14 @@ async function deleteChannel(channelId: string) {
function onChannelContextMenu(event: MouseEvent, channel: any) {
const menuItems: MenuItem[] = [
...(authStore.isAdmin ? [{
label: 'Gérer les permissions',
icon: 'mdi-shield-key',
action: () => {
selectedChannel.value = channel
showPermissionsDialog.value = true
},
}] : []),
{
label: 'Marquer comme lu',
icon: 'mdi-check',
@@ -194,6 +207,12 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
@created="refreshServerTree"
/>
<ChannelPermissionsDialog
v-model="showPermissionsDialog"
:channel="selectedChannel"
:server-id="serverId"
/>
<v-main>
<router-view/>
</v-main>
+6 -2
View File
@@ -2,10 +2,11 @@ use crate::core::state::AppState;
use crate::http::context::Superuser;
use crate::http::error::HTTPError;
use crate::domain::dto::user::{CreateUserRequest, UpdateUserRequest, UserResponse};
use crate::domain::dto::user::UserQueryParams;
use crate::routes::user::mapper;
use axum::{
Json,
extract::{Path, State},
extract::{Path, Query, State},
http::StatusCode,
};
use uuid::Uuid;
@@ -28,8 +29,11 @@ use uuid::Uuid;
pub async fn get_all(
_admin: Superuser,
State(state): State<AppState>,
Query(filters): Query<UserQueryParams>,
) -> Result<Json<Vec<UserResponse>>, HTTPError> {
let users = state.repositories.user.get_all().await?;
let users = state.repositories.user.filter(crate::repositories::types::UserFilter {
server_id: filters.server_id,
}).await?;
Ok(Json(
users
.into_iter()