init
This commit is contained in:
@@ -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>
|
||||||
@@ -22,10 +22,11 @@ export function usePermissions() {
|
|||||||
async function parseResponse<T>(response: Response): Promise<T> {
|
async function parseResponse<T>(response: Response): Promise<T> {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json().catch(() => null)
|
const error = await response.json().catch(() => null)
|
||||||
|
const exception = new Error(
|
||||||
throw new Error(
|
|
||||||
error?.message || 'Erreur lors de la gestion des permissions',
|
error?.message || 'Erreur lors de la gestion des permissions',
|
||||||
)
|
) as Error & { status?: number }
|
||||||
|
exception.status = response.status
|
||||||
|
throw exception
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import CreateCategoryDialog from '@/components/category/CreateCategoryDialog.vue
|
|||||||
import {type MenuItem, useContextMenu} from '@/composables/useContextMenu'
|
import {type MenuItem, useContextMenu} from '@/composables/useContextMenu'
|
||||||
import {useUserStore} from "@/stores/user.ts";
|
import {useUserStore} from "@/stores/user.ts";
|
||||||
import {useServerStore} from "@/stores/server.ts";
|
import {useServerStore} from "@/stores/server.ts";
|
||||||
|
import ChannelPermissionsDialog from '@/components/permissions/ChannelPermissionsDialog.vue'
|
||||||
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
serverId: string
|
serverId: string
|
||||||
@@ -22,6 +24,9 @@ const userStore = useUserStore()
|
|||||||
const serverStore = useServerStore()
|
const serverStore = useServerStore()
|
||||||
const {currentTree} = storeToRefs(serverStore)
|
const {currentTree} = storeToRefs(serverStore)
|
||||||
const {openContextMenu} = useContextMenu()
|
const {openContextMenu} = useContextMenu()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const showPermissionsDialog = ref(false)
|
||||||
|
const selectedChannel = ref<any | null>(null)
|
||||||
|
|
||||||
|
|
||||||
const loadServerData = async (targetServerId: string) => {
|
const loadServerData = async (targetServerId: string) => {
|
||||||
@@ -111,6 +116,14 @@ async function deleteChannel(channelId: string) {
|
|||||||
|
|
||||||
function onChannelContextMenu(event: MouseEvent, channel: any) {
|
function onChannelContextMenu(event: MouseEvent, channel: any) {
|
||||||
const menuItems: MenuItem[] = [
|
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',
|
label: 'Marquer comme lu',
|
||||||
icon: 'mdi-check',
|
icon: 'mdi-check',
|
||||||
@@ -194,6 +207,12 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
|
|||||||
@created="refreshServerTree"
|
@created="refreshServerTree"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ChannelPermissionsDialog
|
||||||
|
v-model="showPermissionsDialog"
|
||||||
|
:channel="selectedChannel"
|
||||||
|
:server-id="serverId"
|
||||||
|
/>
|
||||||
|
|
||||||
<v-main>
|
<v-main>
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</v-main>
|
</v-main>
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ use crate::core::state::AppState;
|
|||||||
use crate::http::context::Superuser;
|
use crate::http::context::Superuser;
|
||||||
use crate::http::error::HTTPError;
|
use crate::http::error::HTTPError;
|
||||||
use crate::domain::dto::user::{CreateUserRequest, UpdateUserRequest, UserResponse};
|
use crate::domain::dto::user::{CreateUserRequest, UpdateUserRequest, UserResponse};
|
||||||
|
use crate::domain::dto::user::UserQueryParams;
|
||||||
use crate::routes::user::mapper;
|
use crate::routes::user::mapper;
|
||||||
use axum::{
|
use axum::{
|
||||||
Json,
|
Json,
|
||||||
extract::{Path, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -28,8 +29,11 @@ use uuid::Uuid;
|
|||||||
pub async fn get_all(
|
pub async fn get_all(
|
||||||
_admin: Superuser,
|
_admin: Superuser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
Query(filters): Query<UserQueryParams>,
|
||||||
) -> Result<Json<Vec<UserResponse>>, HTTPError> {
|
) -> 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(
|
Ok(Json(
|
||||||
users
|
users
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
Reference in New Issue
Block a user