From aa486be6e51674052501d5946eb818f559d2bb7d Mon Sep 17 00:00:00 2001 From: Nell Date: Sun, 2 Aug 2026 11:09:50 +0200 Subject: [PATCH] init --- .../permissions/ChannelPermissionsDialog.vue | 186 ++++++++++++------ frontend/src/composables/usePermissions.ts | 10 + frontend/src/types/permissions.ts | 21 +- src/domain/dto/channel.rs | 6 + src/repositories/channel.rs | 20 ++ src/routes/channel/handlers.rs | 22 ++- src/routes/channel/mapper.rs | 12 +- src/routes/channel/routes.rs | 4 + 8 files changed, 221 insertions(+), 60 deletions(-) diff --git a/frontend/src/components/permissions/ChannelPermissionsDialog.vue b/frontend/src/components/permissions/ChannelPermissionsDialog.vue index 7912d0a..21a5cdc 100644 --- a/frontend/src/components/permissions/ChannelPermissionsDialog.vue +++ b/frontend/src/components/permissions/ChannelPermissionsDialog.vue @@ -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([]) -const targetType = ref<'role' | 'user'>('role') +const configured = ref({users: [], roles: []}) +const targetType = ref('role') const targetId = ref(null) +const addTargetId = ref(null) const mask = ref(toChannelPermissionMask(0)) -const loadingTargets = ref(false) -const loadingPermission = ref(false) +const loading = ref(false) const saving = ref(false) const error = ref(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 } + + diff --git a/frontend/src/composables/usePermissions.ts b/frontend/src/composables/usePermissions.ts index df2b47b..87432c0 100644 --- a/frontend/src/composables/usePermissions.ts +++ b/frontend/src/composables/usePermissions.ts @@ -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 { + const response = await api.get(`/channels/${channelId}/permissions`) + const dto = await parseResponse(response) + return channelPermissionsFromDto(dto) + } + async function getChannelUserPermission( channelId: string, userId: string, @@ -217,6 +226,7 @@ export function usePermissions() { } return { + getChannelPermissions, getServerUserPermission, setServerUserPermission, removeServerUserPermission, diff --git a/frontend/src/types/permissions.ts b/frontend/src/types/permissions.ts index 4934235..9440f7a 100644 --- a/frontend/src/types/permissions.ts +++ b/frontend/src/types/permissions.ts @@ -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), } -} \ No newline at end of file +} + +export function channelPermissionsFromDto( + dto: ChannelPermissionsDto, +): ChannelPermissions { + return { + users: dto.users.map(channelUserPermissionFromDto), + roles: dto.roles.map(channelRolePermissionFromDto), + } +} diff --git a/src/domain/dto/channel.rs b/src/domain/dto/channel.rs index 202f3b4..e09a658 100644 --- a/src/domain/dto/channel.rs +++ b/src/domain/dto/channel.rs @@ -70,3 +70,9 @@ pub struct ChannelRolePermissionResponse { pub role_id: Uuid, pub permissions: u64, } + +#[derive(Debug, Serialize, ToSchema)] +pub struct ChannelPermissionsResponse { + pub users: Vec, + pub roles: Vec, +} diff --git a/src/repositories/channel.rs b/src/repositories/channel.rs index 07f8919..9961a22 100644 --- a/src/repositories/channel.rs +++ b/src/repositories/channel.rs @@ -59,6 +59,16 @@ impl ChannelRepository { .await?) } + pub async fn list_user_permissions( + &self, + channel_id: Uuid, + ) -> AnyResult> { + Ok(channel_user_permission::Entity::find() + .filter(channel_user_permission::Column::ChannelId.eq(channel_id)) + .all(&self.context.db) + .await?) + } + pub async fn set_user_permission( &self, channel_id: Uuid, @@ -109,6 +119,16 @@ impl ChannelRepository { .await?) } + pub async fn list_role_permissions( + &self, + channel_id: Uuid, + ) -> AnyResult> { + Ok(channel_role_permission::Entity::find() + .filter(channel_role_permission::Column::ChannelId.eq(channel_id)) + .all(&self.context.db) + .await?) + } + pub async fn set_role_permission( &self, channel_id: Uuid, diff --git a/src/routes/channel/handlers.rs b/src/routes/channel/handlers.rs index d13b146..fc10b90 100644 --- a/src/routes/channel/handlers.rs +++ b/src/routes/channel/handlers.rs @@ -2,7 +2,7 @@ use crate::core::state::AppState; use crate::http::context::Superuser; use crate::http::error::HTTPError; use crate::domain::dto::channel::{ - ChannelQueryParams, ChannelResponse, ChannelRolePermissionResponse, + ChannelQueryParams, ChannelResponse, ChannelPermissionsResponse, ChannelRolePermissionResponse, ChannelUserPermissionResponse, CreateChannelRequest, SetChannelPermissionRequest, UpdateChannelRequest, }; @@ -69,6 +69,26 @@ pub async fn get_by_id( Ok(Json(mapper::channel_model_to_channel_response(channel))) } +/// Liste les permissions directes configurées pour un canal. +#[utoipa::path( + get, + path = "/channels/{channel_id}/permissions", + params(("channel_id" = Uuid, Path, description = "ID du canal")), + responses((status = 200, body = ChannelPermissionsResponse), (status = 404, description = "Canal non trouvé")), + tag = "Channel Permissions" +)] +pub async fn list_permissions( + State(state): State, + Path(channel_id): Path, +) -> Result, HTTPError> { + state.repositories.channel.get_by_id(channel_id).await?.ok_or(HTTPError::NotFound)?; + let (users, roles) = tokio::try_join!( + state.repositories.channel.list_user_permissions(channel_id), + state.repositories.channel.list_role_permissions(channel_id), + )?; + Ok(Json(mapper::channel_permissions_to_response(users, roles))) +} + /// Crée un nouveau channel #[utoipa::path( post, diff --git a/src/routes/channel/mapper.rs b/src/routes/channel/mapper.rs index 0958368..604fb1f 100644 --- a/src/routes/channel/mapper.rs +++ b/src/routes/channel/mapper.rs @@ -1,5 +1,5 @@ use crate::domain::dto::channel::{ - ChannelQueryParams, ChannelResponse, ChannelRolePermissionResponse, + ChannelPermissionsResponse, ChannelQueryParams, ChannelResponse, ChannelRolePermissionResponse, ChannelUserPermissionResponse, CreateChannelRequest, UpdateChannelRequest, }; use crate::models::{channel, channel_role_permission, channel_user_permission}; @@ -71,6 +71,16 @@ pub fn channel_role_permission_to_response( } } +pub fn channel_permissions_to_response( + users: Vec, + roles: Vec, +) -> ChannelPermissionsResponse { + ChannelPermissionsResponse { + users: users.into_iter().map(channel_user_permission_to_response).collect(), + roles: roles.into_iter().map(channel_role_permission_to_response).collect(), + } +} + pub fn query_params_to_channel_filter(params: ChannelQueryParams) -> ChannelFilter { ChannelFilter { server_id: params.server_id, diff --git a/src/routes/channel/routes.rs b/src/routes/channel/routes.rs index 5ad54ce..3762942 100644 --- a/src/routes/channel/routes.rs +++ b/src/routes/channel/routes.rs @@ -11,6 +11,10 @@ pub fn router() -> Router { .put(handlers::update) .delete(handlers::delete), ) + .route( + "/channels/{channel_id}/permissions", + get(handlers::list_permissions), + ) .route( "/channels/{channel_id}/permissions/users/{user_id}", get(handlers::get_user_permission)