diff --git a/frontend/src/components/permissions/ChannelPermissionsDialog.vue b/frontend/src/components/permissions/ChannelPermissionsDialog.vue
index 21a5cdc..e871f9f 100644
--- a/frontend/src/components/permissions/ChannelPermissionsDialog.vue
+++ b/frontend/src/components/permissions/ChannelPermissionsDialog.vue
@@ -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)
diff --git a/frontend/src/components/server/ServerSettingsDialog.vue b/frontend/src/components/server/ServerSettingsDialog.vue
new file mode 100644
index 0000000..83a1aad
--- /dev/null
+++ b/frontend/src/components/server/ServerSettingsDialog.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+ Paramètres du serveur
+
+ {{ error }}
+
+
+
+
+
+ Général
+
+ Enregistrer
+
+
+ Rôles
+
+
+
+
+
+
+
+
+ Ajouter un rôle
+
+
+
+
+ EnregistrerSupprimer
+
+
+
+
+
+
+
+
+ Sélectionnez un rôle.
+
+
+
+
+
+
+ Fermer
+
+
+
+
+
diff --git a/frontend/src/layouts/AppLayout.vue b/frontend/src/layouts/AppLayout.vue
index 2e86bd4..fff23a0 100644
--- a/frontend/src/layouts/AppLayout.vue
+++ b/frontend/src/layouts/AppLayout.vue
@@ -1,18 +1,24 @@
@@ -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-model="showUsersDrawer"
/>
+
+
diff --git a/frontend/src/pages/server/index.vue b/frontend/src/pages/server/index.vue
index 8d1884a..5ab401a 100644
--- a/frontend/src/pages/server/index.vue
+++ b/frontend/src/pages/server/index.vue
@@ -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(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) {
-
+
+ Gérer le serveur
+
+
+
diff --git a/frontend/src/stores/role.ts b/frontend/src/stores/role.ts
new file mode 100644
index 0000000..a764435
--- /dev/null
+++ b/frontend/src/stores/role.ts
@@ -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,
+ 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
+ },
+ },
+})
diff --git a/frontend/src/stores/server.ts b/frontend/src/stores/server.ts
index 7e79993..b59c078 100644
--- a/frontend/src/stores/server.ts
+++ b/frontend/src/stores/server.ts
@@ -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();
@@ -80,4 +96,4 @@ export const useServerStore = defineStore("server", {
this.error = null;
}
}
-});
\ No newline at end of file
+});
diff --git a/frontend/src/types/role.ts b/frontend/src/types/role.ts
new file mode 100644
index 0000000..9739683
--- /dev/null
+++ b/frontend/src/types/role.ts
@@ -0,0 +1,7 @@
+export interface Role {
+ id: string
+ server_id: string
+ name: string
+ is_default: boolean
+ created_at: string
+}
diff --git a/src/domain/dto/role.rs b/src/domain/dto/role.rs
index 3d8f61c..5ec0487 100644
--- a/src/domain/dto/role.rs
+++ b/src/domain/dto/role.rs
@@ -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,
+}
+
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct CreateRoleRequest {
pub server_id: Uuid,
diff --git a/src/repositories/role.rs b/src/repositories/role.rs
index 5f2c866..a5bd46f 100644
--- a/src/repositories/role.rs
+++ b/src/repositories/role.rs
@@ -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> {
+ 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 {
+ 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 {
+ let result = role_user::Entity::delete_by_id((role_id, user_id))
+ .exec(&self.context.db)
+ .await?;
+ Ok(result.rows_affected > 0)
+ }
}
diff --git a/src/repositories/server.rs b/src/repositories/server.rs
index b7fbb9d..9dceadc 100644
--- a/src/repositories/server.rs
+++ b/src/repositories/server.rs
@@ -84,6 +84,14 @@ impl ServerRepository {
Ok(true)
}
+ pub async fn get_user(&self, server_id: Uuid, user_id: Uuid) -> AnyResult