init
This commit is contained in:
@@ -65,7 +65,7 @@ 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(`/roles?server_id=${props.serverId}`), 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)
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<script lang="ts" setup>
|
||||
import {computed, ref, watch} from 'vue'
|
||||
import {storeToRefs} from 'pinia'
|
||||
import {useServerStore} from '@/stores/server'
|
||||
import {useRoleStore} from '@/stores/role'
|
||||
import {useUserStore} from '@/stores/user'
|
||||
import {usePermissions} from '@/composables/usePermissions'
|
||||
import {toServerPermissionMask, type ServerPermissionMask} from '@/types/permissions'
|
||||
import ServerPermissionEditor from '@/components/permissions/ServerPermissionEditor.vue'
|
||||
|
||||
const props = defineProps<{modelValue: boolean; serverId: string; serverName: string}>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
const serverStore = useServerStore()
|
||||
const roleStore = useRoleStore()
|
||||
const userStore = useUserStore()
|
||||
const permissionsApi = usePermissions()
|
||||
const {roles} = storeToRefs(roleStore)
|
||||
const activeTab = ref('general')
|
||||
const selectedRoleId = ref<string | null>(null)
|
||||
const name = ref('')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const roleForm = ref('')
|
||||
const rolePermissions = ref<ServerPermissionMask>(toServerPermissionMask(0))
|
||||
|
||||
const selectedRole = computed(() => roles.value.find(role => role.id === selectedRoleId.value) || null)
|
||||
const selectedMembers = computed(() => selectedRoleId.value ? roleStore.members[selectedRoleId.value] || [] : [])
|
||||
const availableUsers = computed(() => userStore.users.filter(user => !selectedMembers.value.some(member => member.id === user.id)))
|
||||
const memberToAdd = ref<string | null>(null)
|
||||
|
||||
watch(() => props.modelValue, async open => {
|
||||
if (open) await load()
|
||||
})
|
||||
watch(selectedRoleId, async roleId => {
|
||||
if (!roleId) return
|
||||
roleForm.value = selectedRole.value?.name || ''
|
||||
rolePermissions.value = toServerPermissionMask(0)
|
||||
try {
|
||||
await roleStore.fetchMembers(roleId)
|
||||
const permission = await permissionsApi.getServerRolePermission(props.serverId, roleId)
|
||||
rolePermissions.value = permission.permissions
|
||||
} catch (e) {
|
||||
if ((e as Error & {status?: number}).status !== 404) error.value = e instanceof Error ? e.message : 'Erreur de chargement'
|
||||
}
|
||||
})
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
name.value = props.serverName
|
||||
try {
|
||||
await Promise.all([roleStore.fetchRoles(props.serverId), userStore.fetchUsers(props.serverId)])
|
||||
} catch (e) { error.value = e instanceof Error ? e.message : 'Erreur de chargement' }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
async function saveServer() {
|
||||
if (!name.value.trim()) return
|
||||
saving.value = true
|
||||
try { await serverStore.updateServer(props.serverId, {name: name.value.trim()}) }
|
||||
catch (e) { error.value = e instanceof Error ? e.message : 'Erreur de sauvegarde' }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
async function createRole() {
|
||||
if (!roleForm.value.trim()) return
|
||||
try { const role = await roleStore.createRole({server_id: props.serverId, name: roleForm.value.trim()}); selectedRoleId.value = role.id; roleForm.value = '' }
|
||||
catch (e) { error.value = e instanceof Error ? e.message : 'Erreur de création' }
|
||||
}
|
||||
async function saveRole() {
|
||||
if (!selectedRoleId.value || !roleForm.value.trim()) return
|
||||
try {
|
||||
await roleStore.updateRole(selectedRoleId.value, {name: roleForm.value.trim()})
|
||||
await permissionsApi.setServerRolePermission(props.serverId, selectedRoleId.value, rolePermissions.value)
|
||||
} catch (e) { error.value = e instanceof Error ? e.message : 'Erreur de sauvegarde' }
|
||||
}
|
||||
async function deleteRole() {
|
||||
if (!selectedRoleId.value || selectedRole.value?.is_default) return
|
||||
try { await roleStore.deleteRole(selectedRoleId.value); selectedRoleId.value = null }
|
||||
catch (e) { error.value = e instanceof Error ? e.message : 'Erreur de suppression' }
|
||||
}
|
||||
async function addMember() {
|
||||
if (!selectedRoleId.value || !memberToAdd.value) return
|
||||
try { await roleStore.addMember(selectedRoleId.value, memberToAdd.value); memberToAdd.value = null }
|
||||
catch (e) { error.value = e instanceof Error ? e.message : 'Erreur d’ajout' }
|
||||
}
|
||||
async function savePermissions(value: ServerPermissionMask) {
|
||||
if (!selectedRoleId.value) return
|
||||
try { await permissionsApi.setServerRolePermission(props.serverId, selectedRoleId.value, value); rolePermissions.value = value }
|
||||
catch (e) { error.value = e instanceof Error ? e.message : 'Erreur de permissions' }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog :model-value="modelValue" max-width="980" @update:model-value="emit('update:modelValue', $event)">
|
||||
<v-card min-height="620">
|
||||
<v-card-title>Paramètres du serveur</v-card-title>
|
||||
<v-card-text>
|
||||
<v-alert v-if="error" type="error" density="compact" class="mb-4">{{ error }}</v-alert>
|
||||
<v-row class="settings-layout" no-gutters>
|
||||
<v-col cols="12" md="3" class="settings-sidebar">
|
||||
<v-list density="compact" nav>
|
||||
<v-list-item title="Général" prepend-icon="mdi-cog" :active="activeTab === 'general'" @click="activeTab = 'general'" />
|
||||
<v-list-item title="Rôles" prepend-icon="mdi-shield-account" :active="activeTab === 'roles'" @click="activeTab = 'roles'" />
|
||||
</v-list>
|
||||
</v-col>
|
||||
<v-col cols="12" md="9" class="pa-5">
|
||||
<v-progress-linear v-if="loading" indeterminate class="mb-4" />
|
||||
<template v-if="activeTab === 'general'">
|
||||
<div class="text-h6 mb-4">Général</div>
|
||||
<v-text-field v-model="name" label="Nom du serveur" :disabled="loading" />
|
||||
<v-btn color="primary" :loading="saving" :disabled="!name.trim()" @click="saveServer">Enregistrer</v-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-h6 mb-4">Rôles</div>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-list border density="compact" class="role-list">
|
||||
<v-list-item v-for="role in roles" :key="role.id" :title="role.name" :active="role.id === selectedRoleId" @click="selectedRoleId = role.id">
|
||||
<template #append><v-icon v-if="role.is_default" icon="mdi-star" size="small" /></template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-text-field v-model="roleForm" class="mt-3" label="Nouveau rôle" hide-details @keyup.enter="createRole" />
|
||||
<v-btn class="mt-2" block color="primary" variant="tonal" @click="createRole">Ajouter un rôle</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8">
|
||||
<template v-if="selectedRole">
|
||||
<v-text-field v-model="roleForm" label="Nom du rôle" />
|
||||
<div class="d-flex ga-2 mb-4"><v-btn color="primary" @click="saveRole">Enregistrer</v-btn><v-btn v-if="!selectedRole.is_default" color="error" variant="text" @click="deleteRole">Supprimer</v-btn></div>
|
||||
<v-select v-model="memberToAdd" :items="availableUsers" item-title="username" item-value="id" label="Ajouter un membre" clearable @update:model-value="addMember" />
|
||||
<v-list density="compact" border class="mb-4">
|
||||
<v-list-item v-for="member in selectedMembers" :key="member.id" :title="member.username">
|
||||
<template #append><v-btn icon="mdi-close" size="small" variant="text" @click="roleStore.removeMember(selectedRoleId!, member.id)" /></template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<ServerPermissionEditor v-model="rolePermissions" title="Permissions du rôle" @save="savePermissions" />
|
||||
</template>
|
||||
<v-sheet v-else class="empty-selection d-flex align-center justify-center text-medium-emphasis" border rounded>Sélectionnez un rôle.</v-sheet>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions><v-spacer /><v-btn variant="text" @click="emit('update:modelValue', false)">Fermer</v-btn></v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-layout { min-height: 500px; }
|
||||
.settings-sidebar { border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); }
|
||||
.role-list { max-height: 250px; overflow-y: auto; }
|
||||
.empty-selection { min-height: 400px; }
|
||||
@media (max-width: 959px) { .settings-sidebar { border-right: 0; border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); } }
|
||||
</style>
|
||||
@@ -1,18 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import {storeToRefs} from 'pinia'
|
||||
import {useServerStore} from '@/stores/server'
|
||||
import {useServerStore, type Server} from '@/stores/server'
|
||||
import {computed, ref, watch} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import ContextMenu from "@/components/ContextMenu.vue";
|
||||
import UserListDrawer from '@/components/UserListDrawer.vue'
|
||||
import ServerSettingsDialog from '@/components/server/ServerSettingsDialog.vue'
|
||||
import {useContextMenu} from '@/composables/useContextMenu'
|
||||
|
||||
const serverStore = useServerStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const {openContextMenu} = useContextMenu()
|
||||
|
||||
const {servers} = storeToRefs(serverStore)
|
||||
|
||||
const showUsersDrawer = ref(false)
|
||||
const showServerSettings = ref(false)
|
||||
const selectedServerId = ref<string | null>(null)
|
||||
const selectedServerName = ref('')
|
||||
const isServerContext = computed(() => Boolean(route.params.serverId))
|
||||
|
||||
watch(isServerContext, (isActive) => {
|
||||
@@ -82,6 +88,18 @@ const getServerColor = (str: string): string => {
|
||||
return `hsl(${hue}, 60%, 45%)`
|
||||
}
|
||||
|
||||
function onServerContextMenu(event: MouseEvent, server: Server) {
|
||||
openContextMenu(event, [{
|
||||
label: 'Gérer le serveur',
|
||||
icon: 'mdi-cog',
|
||||
action: () => {
|
||||
selectedServerId.value = server.id
|
||||
selectedServerName.value = server.name
|
||||
showServerSettings.value = true
|
||||
},
|
||||
}])
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -125,6 +143,7 @@ const getServerColor = (str: string): string => {
|
||||
v-for="server in servers"
|
||||
:key="server.id"
|
||||
:to="`/server/${server.id}`"
|
||||
@contextmenu="onServerContextMenu($event, server)"
|
||||
>
|
||||
<v-avatar
|
||||
:style="{ backgroundColor: getServerColor(server.name) }"
|
||||
@@ -150,6 +169,13 @@ const getServerColor = (str: string): string => {
|
||||
v-model="showUsersDrawer"
|
||||
/>
|
||||
|
||||
<ServerSettingsDialog
|
||||
v-if="selectedServerId"
|
||||
v-model="showServerSettings"
|
||||
:server-id="selectedServerId"
|
||||
:server-name="selectedServerName"
|
||||
/>
|
||||
|
||||
<router-view/>
|
||||
<!-- Menu contextuel global -->
|
||||
<ContextMenu/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {storeToRefs} from 'pinia'
|
||||
import {useChannelStore} from '@/stores/channel'
|
||||
import {useCategoryStore} from '@/stores/category'
|
||||
import {ref, watch} from 'vue'
|
||||
import {computed, ref, watch} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import CreateChannelDialog from '@/components/channel/CreateChannelDialog.vue'
|
||||
import CreateCategoryDialog from '@/components/category/CreateCategoryDialog.vue'
|
||||
@@ -11,6 +11,7 @@ import {useUserStore} from "@/stores/user.ts";
|
||||
import {useServerStore} from "@/stores/server.ts";
|
||||
import ChannelPermissionsDialog from '@/components/permissions/ChannelPermissionsDialog.vue'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import ServerSettingsDialog from '@/components/server/ServerSettingsDialog.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
serverId: string
|
||||
@@ -27,6 +28,8 @@ const {openContextMenu} = useContextMenu()
|
||||
const authStore = useAuthStore()
|
||||
const showPermissionsDialog = ref(false)
|
||||
const selectedChannel = ref<any | null>(null)
|
||||
const showServerSettings = ref(false)
|
||||
const serverName = computed(() => serverStore.servers.find(server => server.id === props.serverId)?.name || 'Serveur')
|
||||
|
||||
|
||||
const loadServerData = async (targetServerId: string) => {
|
||||
@@ -149,11 +152,9 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer width="244" @contextmenu="onSidebarContextMenu">
|
||||
<v-sheet
|
||||
color="grey-lighten-5"
|
||||
height="128"
|
||||
width="100%"
|
||||
></v-sheet>
|
||||
<v-sheet color="grey-lighten-5" height="128" width="100%" class="pa-3">
|
||||
<v-btn block variant="text" prepend-icon="mdi-cog" @click="showServerSettings = true">Gérer le serveur</v-btn>
|
||||
</v-sheet>
|
||||
|
||||
<v-list
|
||||
v-model:opened="openedCategories"
|
||||
@@ -213,6 +214,12 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
|
||||
:server-id="serverId"
|
||||
/>
|
||||
|
||||
<ServerSettingsDialog
|
||||
v-model="showServerSettings"
|
||||
:server-id="serverId"
|
||||
:server-name="serverName"
|
||||
/>
|
||||
|
||||
<v-main>
|
||||
<router-view/>
|
||||
</v-main>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {useApi} from '@/composables/useApi'
|
||||
import type {Role} from '@/types/role'
|
||||
import type {User} from '@/types/user'
|
||||
|
||||
export const useRoleStore = defineStore('role', {
|
||||
state: () => ({
|
||||
roles: [] as Role[],
|
||||
members: {} as Record<string, User[]>,
|
||||
loading: false,
|
||||
}),
|
||||
actions: {
|
||||
async fetchRoles(serverId: string) {
|
||||
const response = await useApi().get(`/roles?server_id=${serverId}`)
|
||||
if (!response.ok) throw new Error('Impossible de charger les rôles')
|
||||
this.roles = await response.json()
|
||||
return this.roles
|
||||
},
|
||||
async createRole(payload: {server_id: string; name: string; is_default?: boolean}) {
|
||||
const response = await useApi().post('/roles', payload)
|
||||
if (!response.ok) throw new Error('Impossible de créer le rôle')
|
||||
const role = await response.json()
|
||||
this.roles.push(role)
|
||||
return role
|
||||
},
|
||||
async updateRole(id: string, payload: {name: string; is_default?: boolean}) {
|
||||
const response = await useApi().put(`/roles/${id}`, payload)
|
||||
if (!response.ok) throw new Error('Impossible de modifier le rôle')
|
||||
const role = await response.json()
|
||||
const index = this.roles.findIndex(item => item.id === id)
|
||||
if (index >= 0) this.roles[index] = role
|
||||
return role
|
||||
},
|
||||
async deleteRole(id: string) {
|
||||
const response = await useApi().delete(`/roles/${id}`)
|
||||
if (!response.ok) throw new Error('Impossible de supprimer le rôle')
|
||||
this.roles = this.roles.filter(role => role.id !== id)
|
||||
delete this.members[id]
|
||||
},
|
||||
async fetchMembers(roleId: string) {
|
||||
const response = await useApi().get(`/roles/${roleId}/members`)
|
||||
if (!response.ok) throw new Error('Impossible de charger les membres')
|
||||
this.members[roleId] = await response.json()
|
||||
return this.members[roleId]
|
||||
},
|
||||
async addMember(roleId: string, userId: string) {
|
||||
const response = await useApi().put(`/roles/${roleId}/members/${userId}`)
|
||||
if (!response.ok) throw new Error('Impossible d’ajouter le membre')
|
||||
await this.fetchMembers(roleId)
|
||||
},
|
||||
async removeMember(roleId: string, userId: string) {
|
||||
const response = await useApi().delete(`/roles/${roleId}/members/${userId}`)
|
||||
if (!response.ok) throw new Error('Impossible de retirer le membre')
|
||||
await this.fetchMembers(roleId)
|
||||
},
|
||||
reset() {
|
||||
this.roles = []
|
||||
this.members = {}
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import {useApi} from "@/composables/useApi.ts";
|
||||
import {useChannelStore} from "@/stores/channel.ts";
|
||||
import {useCategoryStore} from "@/stores/category.ts";
|
||||
|
||||
interface Server {
|
||||
export interface Server {
|
||||
id: string
|
||||
name: string
|
||||
is_default: boolean
|
||||
@@ -44,6 +44,22 @@ export const useServerStore = defineStore("server", {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async updateServer(serverId: string, payload: { name: string; is_default?: boolean }) {
|
||||
const api = useApi();
|
||||
const response = await api.put(`/servers/${serverId}`, {
|
||||
name: payload.name,
|
||||
password: null,
|
||||
is_default: payload.is_default ?? false,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => null);
|
||||
throw new Error(error?.message || 'Failed to update server');
|
||||
}
|
||||
const updated = await response.json();
|
||||
const index = this.servers.findIndex(server => server.id === serverId);
|
||||
if (index >= 0) this.servers[index] = updated;
|
||||
return updated;
|
||||
},
|
||||
async fetchServerTree(serverId: string) {
|
||||
const api = useApi();
|
||||
const channelStore = useChannelStore();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface Role {
|
||||
id: string
|
||||
server_id: string
|
||||
name: string
|
||||
is_default: boolean
|
||||
created_at: string
|
||||
}
|
||||
@@ -3,6 +3,11 @@ use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema, utoipa::IntoParams)]
|
||||
pub struct RoleQueryParams {
|
||||
pub server_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CreateRoleRequest {
|
||||
pub server_id: Uuid,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::models::{role, role_user};
|
||||
use crate::models::{role, role_user, user};
|
||||
use crate::repositories::{AnyResult, RepositoryContext};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||
use std::sync::Arc;
|
||||
@@ -63,4 +63,49 @@ impl RoleRepository {
|
||||
.await?;
|
||||
Ok(res.rows_affected > 0)
|
||||
}
|
||||
|
||||
pub async fn get_members(&self, role_id: Uuid) -> AnyResult<Vec<user::Model>> {
|
||||
let memberships = role_user::Entity::find()
|
||||
.filter(role_user::Column::RoleId.eq(role_id))
|
||||
.all(&self.context.db)
|
||||
.await?;
|
||||
|
||||
let mut members = Vec::with_capacity(memberships.len());
|
||||
for membership in memberships {
|
||||
if let Some(user) = user::Entity::find_by_id(membership.user_id)
|
||||
.one(&self.context.db)
|
||||
.await?
|
||||
{
|
||||
members.push(user);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(members)
|
||||
}
|
||||
|
||||
pub async fn add_member(&self, role_id: Uuid, user_id: Uuid) -> AnyResult<bool> {
|
||||
if role_user::Entity::find_by_id((role_id, user_id))
|
||||
.one(&self.context.db)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
role_user::ActiveModel {
|
||||
role_id: Set(role_id),
|
||||
user_id: Set(user_id),
|
||||
}
|
||||
.insert(&self.context.db)
|
||||
.await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn remove_member(&self, role_id: Uuid, user_id: Uuid) -> AnyResult<bool> {
|
||||
let result = role_user::Entity::delete_by_id((role_id, user_id))
|
||||
.exec(&self.context.db)
|
||||
.await?;
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,14 @@ impl ServerRepository {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn get_user(&self, server_id: Uuid, user_id: Uuid) -> AnyResult<Option<server_user::Model>> {
|
||||
Ok(server_user::Entity::find()
|
||||
.filter(server_user::Column::ServerId.eq(server_id))
|
||||
.filter(server_user::Column::UserId.eq(user_id))
|
||||
.one(&self.context.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: Uuid) -> AnyResult<bool> {
|
||||
let res = server::Entity::delete_by_id(id)
|
||||
.exec(&self.context.db)
|
||||
|
||||
+95
-126
@@ -1,162 +1,131 @@
|
||||
use crate::core::state::AppState;
|
||||
use crate::http::context::Superuser;
|
||||
use crate::domain::dto::role::{CreateRoleRequest, RoleQueryParams, RoleResponse, UpdateRoleRequest};
|
||||
use crate::domain::dto::user::UserResponse;
|
||||
use crate::http::context::CurrentUser;
|
||||
use crate::http::error::HTTPError;
|
||||
use crate::domain::dto::role::{CreateRoleRequest, RoleResponse, UpdateRoleRequest};
|
||||
use crate::permissions::ServerPermission;
|
||||
use crate::routes::role::mapper;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use crate::routes::user::mapper as user_mapper;
|
||||
use axum::{Json, extract::{Path, Query, State}, http::StatusCode};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Liste tous les groupes
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/groups",
|
||||
responses(
|
||||
(status = 200, description = "Liste des groupes récupérée avec succès", body = [RoleResponse]),
|
||||
(status = 500, description = "Erreur interne du serveur")
|
||||
),
|
||||
tag = "Roles"
|
||||
)]
|
||||
pub async fn get_all(State(state): State<AppState>) -> Result<Json<Vec<RoleResponse>>, HTTPError> {
|
||||
let groups = state.repositories.role.get_all().await?;
|
||||
Ok(Json(
|
||||
groups
|
||||
.into_iter()
|
||||
.map(mapper::group_model_to_group_response)
|
||||
.collect(),
|
||||
))
|
||||
async fn require_permission(
|
||||
state: &AppState,
|
||||
user: &CurrentUser,
|
||||
server_id: Uuid,
|
||||
permission: ServerPermission,
|
||||
) -> Result<(), HTTPError> {
|
||||
if user.is_superuser {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Récupère un groupe par son ID
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/groups/{id}",
|
||||
responses(
|
||||
(status = 200, description = "Rolee trouvé", body = RoleResponse),
|
||||
(status = 404, description = "Rolee non trouvé"),
|
||||
(status = 500, description = "Erreur interne du serveur")
|
||||
),
|
||||
params(
|
||||
("id" = Uuid, Path, description = "ID du groupe")
|
||||
),
|
||||
tag = "Roles"
|
||||
)]
|
||||
let granted = state
|
||||
.repositories
|
||||
.server
|
||||
.get_user_permission(server_id, user.id)
|
||||
.await?
|
||||
.map(|value| ServerPermission::from_bits_truncate(value.permissions as u64))
|
||||
.unwrap_or_default();
|
||||
|
||||
if granted.contains(permission) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(HTTPError::Forbidden)
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/roles", params(RoleQueryParams), responses((status = 200, body = [RoleResponse])), tag = "Roles")]
|
||||
pub async fn get_all(
|
||||
State(state): State<AppState>,
|
||||
Query(filters): Query<RoleQueryParams>,
|
||||
) -> Result<Json<Vec<RoleResponse>>, HTTPError> {
|
||||
let roles = match filters.server_id {
|
||||
Some(server_id) => state.repositories.role.get_all_by_server(server_id).await?,
|
||||
None => state.repositories.role.get_all().await?,
|
||||
};
|
||||
|
||||
Ok(Json(roles.into_iter().map(mapper::role_model_to_role_response).collect()))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/roles/{id}", params(("id" = Uuid, Path)), responses((status = 200, body = RoleResponse), (status = 404)), tag = "Roles")]
|
||||
pub async fn get_by_id(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<RoleResponse>, HTTPError> {
|
||||
let group = state
|
||||
.repositories
|
||||
.role
|
||||
.get_by_id(id)
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
Ok(Json(mapper::group_model_to_group_response(group)))
|
||||
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?;
|
||||
Ok(Json(mapper::role_model_to_role_response(role)))
|
||||
}
|
||||
|
||||
/// Crée un nouveau groupe
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/groups",
|
||||
request_body = CreateRoleRequest,
|
||||
responses(
|
||||
(status = 201, description = "Role créé avec succès", body = RoleResponse),
|
||||
(status = 404, description = "Serveur non trouvé"),
|
||||
(status = 500, description = "Erreur interne du serveur")
|
||||
),
|
||||
tag = "Roles",
|
||||
security(
|
||||
("bearerAuth" = [])
|
||||
)
|
||||
)]
|
||||
#[utoipa::path(post, path = "/roles", request_body = CreateRoleRequest, responses((status = 201, body = RoleResponse)), tag = "Roles", security(("bearerAuth" = [])))]
|
||||
pub async fn create(
|
||||
_admin: Superuser,
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreateRoleRequest>,
|
||||
) -> Result<(StatusCode, Json<RoleResponse>), HTTPError> {
|
||||
// Vérifier que le serveur existe
|
||||
state
|
||||
.repositories
|
||||
.server
|
||||
.get_by_id(payload.server_id)
|
||||
.await?
|
||||
state.repositories.server.get_by_id(payload.server_id).await?
|
||||
.ok_or(HTTPError::BadRequest("Server not found".to_string()))?;
|
||||
require_permission(&state, &user, payload.server_id, ServerPermission::MANAGE_ROLES).await?;
|
||||
|
||||
let active_model = mapper::create_request_to_am(payload);
|
||||
let group = state.services.role.create_role(active_model).await?;
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(mapper::group_model_to_group_response(group)),
|
||||
))
|
||||
let role = state.services.role.create_role(mapper::create_request_to_am(payload)).await?;
|
||||
Ok((StatusCode::CREATED, Json(mapper::role_model_to_role_response(role))))
|
||||
}
|
||||
|
||||
/// Met à jour un groupe existant
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/groups/{id}",
|
||||
request_body = UpdateRoleRequest,
|
||||
responses(
|
||||
(status = 200, description = "Role mis à jour avec succès", body = RoleResponse),
|
||||
(status = 404, description = "Role non trouvé"),
|
||||
(status = 500, description = "Erreur interne du serveur")
|
||||
),
|
||||
params(
|
||||
("id" = Uuid, Path, description = "ID du groupe")
|
||||
),
|
||||
tag = "Roles",
|
||||
security(
|
||||
("bearerAuth" = [])
|
||||
)
|
||||
)]
|
||||
#[utoipa::path(put, path = "/roles/{id}", request_body = UpdateRoleRequest, responses((status = 200, body = RoleResponse), (status = 404)), tag = "Roles", security(("bearerAuth" = [])))]
|
||||
pub async fn update(
|
||||
_admin: Superuser,
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateRoleRequest>,
|
||||
) -> Result<Json<RoleResponse>, HTTPError> {
|
||||
// Vérifier l'existence
|
||||
let group = state
|
||||
.repositories
|
||||
.role
|
||||
.get_by_id(id)
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?;
|
||||
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_ROLES).await?;
|
||||
|
||||
let active_model = mapper::update_request_to_am(group.id, group.server_id, payload);
|
||||
let group = state.services.role.update_role(active_model).await?;
|
||||
|
||||
Ok(Json(mapper::group_model_to_group_response(group)))
|
||||
let role = state.services.role.update_role(mapper::update_request_to_am(role.id, role.server_id, payload)).await?;
|
||||
Ok(Json(mapper::role_model_to_role_response(role)))
|
||||
}
|
||||
|
||||
/// Supprime un groupe
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/groups/{id}",
|
||||
responses(
|
||||
(status = 204, description = "Role supprimé avec succès"),
|
||||
(status = 404, description = "Role non trouvé"),
|
||||
(status = 500, description = "Erreur interne du serveur")
|
||||
),
|
||||
params(
|
||||
("id" = Uuid, Path, description = "ID du groupe")
|
||||
),
|
||||
tag = "Roles",
|
||||
security(
|
||||
("bearerAuth" = [])
|
||||
)
|
||||
)]
|
||||
#[utoipa::path(delete, path = "/roles/{id}", responses((status = 204), (status = 404)), tag = "Roles", security(("bearerAuth" = [])))]
|
||||
pub async fn delete(
|
||||
_admin: Superuser,
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, HTTPError> {
|
||||
if state.services.role.delete_role(id).await? {
|
||||
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?;
|
||||
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_ROLES).await?;
|
||||
|
||||
if state.services.role.delete_role(id).await? { Ok(StatusCode::NO_CONTENT) } else { Err(HTTPError::NotFound) }
|
||||
}
|
||||
|
||||
pub async fn get_members(
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<UserResponse>>, HTTPError> {
|
||||
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?;
|
||||
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_MEMBERS).await?;
|
||||
let members = state.repositories.role.get_members(id).await?;
|
||||
Ok(Json(members.into_iter().map(user_mapper::user_model_to_user_response).collect()))
|
||||
}
|
||||
|
||||
pub async fn add_member(
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Path((id, user_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<StatusCode, HTTPError> {
|
||||
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?;
|
||||
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_MEMBERS).await?;
|
||||
state.repositories.server.get_user(role.server_id, user_id).await?
|
||||
.ok_or(HTTPError::BadRequest("User is not a member of this server".to_string()))?;
|
||||
state.services.role.add_member(id, user_id, role.server_id).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
Err(HTTPError::NotFound)
|
||||
}
|
||||
|
||||
pub async fn remove_member(
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Path((id, user_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<StatusCode, HTTPError> {
|
||||
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?;
|
||||
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_MEMBERS).await?;
|
||||
if state.services.role.remove_member(id, user_id, role.server_id).await? { Ok(StatusCode::NO_CONTENT) } else { Err(HTTPError::NotFound) }
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::domain::dto::role::{CreateRoleRequest, RoleResponse, UpdateRoleReques
|
||||
use sea_orm::Set;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn group_model_to_group_response(model: role::Model) -> RoleResponse {
|
||||
pub fn role_model_to_role_response(model: role::Model) -> RoleResponse {
|
||||
RoleResponse {
|
||||
id: model.id,
|
||||
server_id: model.server_id,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use crate::core::state::AppState;
|
||||
use axum::{routing::get, Router};
|
||||
use axum::{routing::{get, put}, Router};
|
||||
|
||||
use super::handlers;
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/groups", get(handlers::get_all).post(handlers::create))
|
||||
.route("/roles", get(handlers::get_all).post(handlers::create))
|
||||
.route(
|
||||
"/groups/{id}",
|
||||
"/roles/{id}",
|
||||
get(handlers::get_by_id)
|
||||
.put(handlers::update)
|
||||
.delete(handlers::delete),
|
||||
)
|
||||
.route("/roles/{id}/members", get(handlers::get_members))
|
||||
.route("/roles/{id}/members/{user_id}", put(handlers::add_member).delete(handlers::remove_member))
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::domain::dto::server::{
|
||||
};
|
||||
use crate::http::context::{CurrentUser, Superuser};
|
||||
use crate::http::error::HTTPError;
|
||||
use crate::permissions::ServerPermission;
|
||||
use crate::routes::server::mapper;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -13,6 +14,27 @@ use axum::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
async fn require_server_permission(
|
||||
state: &AppState,
|
||||
user: &CurrentUser,
|
||||
server_id: Uuid,
|
||||
permission: ServerPermission,
|
||||
) -> Result<(), HTTPError> {
|
||||
if user.is_superuser {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let granted = state
|
||||
.repositories
|
||||
.server
|
||||
.get_user_permission(server_id, user.id)
|
||||
.await?
|
||||
.map(|value| ServerPermission::from_bits_truncate(value.permissions as u64))
|
||||
.unwrap_or_default();
|
||||
|
||||
if granted.contains(permission) { Ok(()) } else { Err(HTTPError::Forbidden) }
|
||||
}
|
||||
|
||||
/// Liste tous les serveurs
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -112,7 +134,7 @@ pub async fn create(
|
||||
)
|
||||
)]
|
||||
pub async fn update(
|
||||
_admin: Superuser,
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateServerRequest>,
|
||||
@@ -125,6 +147,8 @@ pub async fn update(
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
require_server_permission(&state, &user, id, ServerPermission::MANAGE_SERVER).await?;
|
||||
|
||||
let server = state
|
||||
.services
|
||||
.server
|
||||
@@ -311,10 +335,12 @@ pub async fn get_role_permission(
|
||||
tag = "Server Permissions"
|
||||
)]
|
||||
pub async fn set_role_permission(
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Path((server_id, role_id)): Path<(Uuid, Uuid)>,
|
||||
Json(payload): Json<SetServerPermissionRequest>,
|
||||
) -> Result<Json<ServerRolePermissionResponse>, HTTPError> {
|
||||
require_server_permission(&state, &user, server_id, ServerPermission::MANAGE_ROLES).await?;
|
||||
state
|
||||
.repositories
|
||||
.server
|
||||
@@ -347,9 +373,11 @@ pub async fn set_role_permission(
|
||||
tag = "Server Permissions"
|
||||
)]
|
||||
pub async fn remove_role_permission(
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Path((server_id, role_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<StatusCode, HTTPError> {
|
||||
require_server_permission(&state, &user, server_id, ServerPermission::MANAGE_ROLES).await?;
|
||||
if state
|
||||
.repositories
|
||||
.server
|
||||
|
||||
+28
-8
@@ -1,6 +1,6 @@
|
||||
use crate::services::ServicesContext;
|
||||
use crate::models::role;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, TransactionTrait, Set};
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait, TransactionTrait};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -23,13 +23,13 @@ impl RoleService {
|
||||
|
||||
let txn = db.begin().await?;
|
||||
|
||||
let group = active.insert(&txn).await?;
|
||||
let role = active.insert(&txn).await?;
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
event_bus.emit("group_created", group.clone());
|
||||
event_bus.emit("role_created", role.clone());
|
||||
|
||||
Ok(group)
|
||||
Ok(role)
|
||||
}
|
||||
|
||||
pub async fn update_role(
|
||||
@@ -41,13 +41,13 @@ impl RoleService {
|
||||
|
||||
let txn = db.begin().await?;
|
||||
|
||||
let group = active.update(&txn).await?;
|
||||
let role = active.update(&txn).await?;
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
event_bus.emit("group_updated", group.clone());
|
||||
event_bus.emit("role_updated", role.clone());
|
||||
|
||||
Ok(group)
|
||||
Ok(role)
|
||||
}
|
||||
|
||||
pub async fn delete_role(&self, id: Uuid) -> Result<bool, anyhow::Error> {
|
||||
@@ -65,9 +65,29 @@ impl RoleService {
|
||||
txn.commit().await?;
|
||||
|
||||
if deleted {
|
||||
event_bus.emit("group_deleted", id);
|
||||
event_bus.emit("role_deleted", id);
|
||||
}
|
||||
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
pub async fn add_member(&self, role_id: Uuid, user_id: Uuid, server_id: Uuid) -> Result<bool, anyhow::Error> {
|
||||
let added = self.service_context.repositories.role.add_member(role_id, user_id).await?;
|
||||
if added {
|
||||
self.service_context
|
||||
.event_bus
|
||||
.emit("role_user_created", (role_id, user_id, server_id));
|
||||
}
|
||||
Ok(added)
|
||||
}
|
||||
|
||||
pub async fn remove_member(&self, role_id: Uuid, user_id: Uuid, server_id: Uuid) -> Result<bool, anyhow::Error> {
|
||||
let removed = self.service_context.repositories.role.remove_member(role_id, user_id).await?;
|
||||
if removed {
|
||||
self.service_context
|
||||
.event_bus
|
||||
.emit("role_user_deleted", (role_id, user_id, server_id));
|
||||
}
|
||||
Ok(removed)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user