This commit is contained in:
2026-08-02 12:07:51 +02:00
parent aa486be6e5
commit bb4e17ba2f
15 changed files with 502 additions and 152 deletions
@@ -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 dajout' }
}
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>
+27 -1
View File
@@ -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/>
+13 -6
View File
@@ -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>
+62
View File
@@ -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 dajouter 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
},
},
})
+17 -1
View File
@@ -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();
+7
View File
@@ -0,0 +1,7 @@
export interface Role {
id: string
server_id: string
name: string
is_default: boolean
created_at: string
}
+5
View File
@@ -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,
+46 -1
View File
@@ -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)
}
}
+8
View File
@@ -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)
+96 -127
View File
@@ -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(());
}
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)
}
}
/// 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"
)]
#[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) }
}
+1 -1
View File
@@ -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,
+5 -3
View File
@@ -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))
}
+29 -1
View File
@@ -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
View File
@@ -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)
}
}