Compare commits
2
Commits
9dbb7ffd5b
...
aa486be6e5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa486be6e5 | ||
|
|
659fd0f304 |
@@ -0,0 +1,205 @@
|
||||
<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, 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] }>()
|
||||
|
||||
const users = useUserStore()
|
||||
const api = useApi()
|
||||
const permissionsApi = usePermissions()
|
||||
const roles = ref<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 loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
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 load()
|
||||
else reset()
|
||||
})
|
||||
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 load() {
|
||||
if (!props.channel) return
|
||||
loading.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)
|
||||
configured.value = await permissionsApi.getChannelPermissions(props.channel.id)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Erreur lors du chargement des permissions'
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
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 }
|
||||
}
|
||||
|
||||
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)
|
||||
configured.value = await permissionsApi.getChannelPermissions(props.channel.id)
|
||||
targetId.value = null
|
||||
mask.value = toChannelPermissionMask(0)
|
||||
} catch (e) {
|
||||
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() { configured.value = {users: [], roles: []}; targetId.value = null; addTargetId.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-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,
|
||||
@@ -22,10 +25,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) {
|
||||
@@ -129,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,
|
||||
@@ -216,6 +226,7 @@ export function usePermissions() {
|
||||
}
|
||||
|
||||
return {
|
||||
getChannelPermissions,
|
||||
getServerUserPermission,
|
||||
setServerUserPermission,
|
||||
removeServerUserPermission,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -443,3 +453,12 @@ export function channelRolePermissionFromDto(
|
||||
permissions: toChannelPermissionMask(dto.permissions),
|
||||
}
|
||||
}
|
||||
|
||||
export function channelPermissionsFromDto(
|
||||
dto: ChannelPermissionsDto,
|
||||
): ChannelPermissions {
|
||||
return {
|
||||
users: dto.users.map(channelUserPermissionFromDto),
|
||||
roles: dto.roles.map(channelRolePermissionFromDto),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,3 +70,9 @@ pub struct ChannelRolePermissionResponse {
|
||||
pub role_id: Uuid,
|
||||
pub permissions: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ChannelPermissionsResponse {
|
||||
pub users: Vec<ChannelUserPermissionResponse>,
|
||||
pub roles: Vec<ChannelRolePermissionResponse>,
|
||||
}
|
||||
|
||||
@@ -59,6 +59,16 @@ impl ChannelRepository {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn list_user_permissions(
|
||||
&self,
|
||||
channel_id: Uuid,
|
||||
) -> AnyResult<Vec<channel_user_permission::Model>> {
|
||||
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<Vec<channel_role_permission::Model>> {
|
||||
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,
|
||||
|
||||
@@ -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<AppState>,
|
||||
Path(channel_id): Path<Uuid>,
|
||||
) -> Result<Json<ChannelPermissionsResponse>, 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,
|
||||
|
||||
@@ -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<channel_user_permission::Model>,
|
||||
roles: Vec<channel_role_permission::Model>,
|
||||
) -> 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,
|
||||
|
||||
@@ -11,6 +11,10 @@ pub fn router() -> Router<AppState> {
|
||||
.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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user