From b946cfd8669092ad79f61a8901bb8dee9bee6ac0 Mon Sep 17 00:00:00 2001 From: Nell Date: Sat, 1 Aug 2026 14:11:19 +0200 Subject: [PATCH] init --- .../src/m20220101_000001_create_table.rs | 2 + src/domain/dto/category.rs | 9 +- src/domain/dto/channel.rs | 5 + src/repositories/mod.rs | 2 +- src/repositories/server_tree.rs | 95 ++-- src/repositories/types.rs | 22 +- src/routes/category/mapper.rs | 10 +- src/routes/channel/mapper.rs | 12 +- src/routes/server/handlers.rs | 29 +- src/routes/server/mapper.rs | 63 ++- src/services/channel.rs | 129 ++++-- src/services/mod.rs | 25 +- src/services/permission.rs | 438 ++++++++++++++++++ 13 files changed, 689 insertions(+), 152 deletions(-) create mode 100644 src/services/permission.rs diff --git a/migration/src/m20220101_000001_create_table.rs b/migration/src/m20220101_000001_create_table.rs index db0132b..41a03ec 100644 --- a/migration/src/m20220101_000001_create_table.rs +++ b/migration/src/m20220101_000001_create_table.rs @@ -746,6 +746,7 @@ impl MigrationTrait for Migration { Table::create() .table(Alias::new("computed_permission")) .if_not_exists() + .col(ColumnDef::new(Alias::new("id")).uuid().not_null()) .col(ColumnDef::new(Alias::new("user_id")).uuid().not_null()) .col(ColumnDef::new(Alias::new("server_id")).uuid().not_null()) .col( @@ -762,6 +763,7 @@ impl MigrationTrait for Migration { ) .primary_key( Index::create() + .col(Alias::new("id")) .col(Alias::new("user_id")) .col(Alias::new("server_id")) .col(Alias::new("scope_type")) diff --git a/src/domain/dto/category.rs b/src/domain/dto/category.rs index fdc9875..97f5050 100644 --- a/src/domain/dto/category.rs +++ b/src/domain/dto/category.rs @@ -10,9 +10,7 @@ pub struct CategoryQueryParams { impl Default for CategoryQueryParams { fn default() -> Self { - Self { - server_id: None, - } + Self { server_id: None } } } @@ -36,4 +34,9 @@ pub struct CategoryResponse { pub name: String, pub created_at: DateTime, pub updated_at: DateTime, + + /// None : contexte sans permissions (champ ignoré dans le JSON). + /// Some(value) : valeur de computed_permission (0 si absente). + #[serde(skip_serializing_if = "Option::is_none")] + pub permission: Option, } diff --git a/src/domain/dto/channel.rs b/src/domain/dto/channel.rs index 47c8498..202f3b4 100644 --- a/src/domain/dto/channel.rs +++ b/src/domain/dto/channel.rs @@ -41,6 +41,11 @@ pub struct ChannelResponse { pub name: Option, pub created_at: DateTime, pub updated_at: DateTime, + + /// None : contexte sans permissions (champ ignoré dans le JSON). + /// Some(value) : valeur de computed_permission (0 si absente). + #[serde(skip_serializing_if = "Option::is_none")] + pub permission: Option, } #[derive(Debug, Serialize, Deserialize, ToSchema)] diff --git a/src/repositories/mod.rs b/src/repositories/mod.rs index a3fde4f..e770c97 100644 --- a/src/repositories/mod.rs +++ b/src/repositories/mod.rs @@ -21,7 +21,7 @@ mod server; mod server_item_order; mod server_tree; pub mod types; -mod user; +pub mod user; #[derive(Clone, Debug)] pub struct RepositoryContext { diff --git a/src/repositories/server_tree.rs b/src/repositories/server_tree.rs index 3e5ae9d..79e68e3 100644 --- a/src/repositories/server_tree.rs +++ b/src/repositories/server_tree.rs @@ -1,23 +1,12 @@ use crate::models::{category, channel, computed_permission, server_item_order}; use crate::permissions::ChannelPermission; +use crate::repositories::types::{CategoryWithPermissions, ChannelWithPermissions, ServerTreeData}; use crate::repositories::{AnyResult, RepositoryContext}; -use sea_orm::{ColumnTrait, EntityTrait, JoinType, QueryFilter, QuerySelect}; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; +use std::collections::HashMap; use std::sync::Arc; use uuid::Uuid; -#[derive(Debug, Clone)] -pub struct ChannelWithPermissions { - pub channel: channel::Model, - pub permissions: ChannelPermission, -} - -#[derive(Debug, Clone)] -pub struct ServerTreeData { - pub orders: Vec, - pub categories: Vec, - pub channels: Vec, -} - #[derive(Clone, Debug)] pub struct ServerTreeRepository { pub context: Arc, @@ -25,7 +14,7 @@ pub struct ServerTreeRepository { impl ServerTreeRepository { pub async fn get_for_user(&self, server_id: Uuid, user_id: Uuid) -> AnyResult { - let (orders, categories, channel_rows) = tokio::try_join!( + let (orders, categories_models, channel_models, computed_permissions) = tokio::try_join!( async { Ok::<_, anyhow::Error>( server_item_order::Entity::find() @@ -42,16 +31,53 @@ impl ServerTreeRepository { .await?, ) }, - self.find_channels_with_permissions(server_id, user_id), + async { + Ok::<_, anyhow::Error>( + channel::Entity::find() + .filter(channel::Column::ServerId.eq(server_id)) + .all(&self.context.db) + .await?, + ) + }, + async { + Ok::<_, anyhow::Error>( + computed_permission::Entity::find() + .filter(computed_permission::Column::UserId.eq(user_id)) + .filter(computed_permission::Column::ServerId.eq(server_id)) + .all(&self.context.db) + .await?, + ) + }, )?; - let channels = channel_rows + let perm_map: HashMap = computed_permissions .into_iter() - .filter_map(|(channel, permission)| { - permission.map(|permission| ChannelWithPermissions { + .map(|cp| (cp.resource_id, cp.permissions as u64)) + .collect(); + + let categories = categories_models + .into_iter() + .map(|category| { + let permissions = perm_map + .get(&category.id) + .map(|&p| ChannelPermission::from_bits_retain(p)); + CategoryWithPermissions { + category, + permissions, + } + }) + .collect(); + + let channels = channel_models + .into_iter() + .map(|channel| { + let permissions = perm_map + .get(&channel.id) + .map(|&p| ChannelPermission::from_bits_retain(p)); + ChannelWithPermissions { channel, - permissions: ChannelPermission::from_bits_retain(permission.permissions as u64), - }) + permissions, + } }) .collect(); @@ -61,31 +87,4 @@ impl ServerTreeRepository { channels, }) } - - async fn find_channels_with_permissions( - &self, - server_id: Uuid, - user_id: Uuid, - ) -> AnyResult)>> { - // Construction dynamique de la relation personnalisée (channel.id = computed_permission.resource_id) - let channel_to_permission = channel::Entity::belongs_to(computed_permission::Entity) - .from(channel::Column::Id) - .to(computed_permission::Column::ResourceId) - .into(); - - let rows = channel::Entity::find() - .join(JoinType::InnerJoin, channel_to_permission) - .filter(channel::Column::ServerId.eq(server_id)) - .filter(computed_permission::Column::UserId.eq(user_id)) - .filter(computed_permission::Column::ServerId.eq(server_id)) - .filter( - computed_permission::Column::ScopeType - .eq(computed_permission::PermissionScopeType::Channel), - ) - .select_also(computed_permission::Entity) - .all(&self.context.db) - .await?; - - Ok(rows) - } } diff --git a/src/repositories/types.rs b/src/repositories/types.rs index 582afec..92c20ca 100644 --- a/src/repositories/types.rs +++ b/src/repositories/types.rs @@ -1,4 +1,5 @@ -use crate::models::{category, channel, computed_permission}; +use crate::models::{category, channel, computed_permission, server_item_order}; +use crate::permissions::ChannelPermission; use uuid::Uuid; pub enum ServerExplorerItem { @@ -56,3 +57,22 @@ impl PermissionResource { } } } + +#[derive(Debug, Clone)] +pub struct CategoryWithPermissions { + pub category: category::Model, + pub permissions: Option, +} + +#[derive(Debug, Clone)] +pub struct ChannelWithPermissions { + pub channel: channel::Model, + pub permissions: Option, +} + +#[derive(Debug, Clone)] +pub struct ServerTreeData { + pub orders: Vec, + pub categories: Vec, + pub channels: Vec, +} diff --git a/src/routes/category/mapper.rs b/src/routes/category/mapper.rs index 6b3e878..b8798a2 100644 --- a/src/routes/category/mapper.rs +++ b/src/routes/category/mapper.rs @@ -1,17 +1,25 @@ -use crate::models::category; use crate::domain::dto::category::{ CategoryResponse, CreateCategoryRequest, UpdateCategoryRequest, }; +use crate::models::category; use sea_orm::Set; use uuid::Uuid; pub fn category_model_to_category_response(model: category::Model) -> CategoryResponse { + category_model_to_category_response_with_permission(model, None) +} + +pub fn category_model_to_category_response_with_permission( + model: category::Model, + permission: Option, +) -> CategoryResponse { CategoryResponse { id: model.id, server_id: model.server_id, name: model.name, created_at: model.created_at, updated_at: model.updated_at, + permission, } } diff --git a/src/routes/channel/mapper.rs b/src/routes/channel/mapper.rs index 0f4ae30..0958368 100644 --- a/src/routes/channel/mapper.rs +++ b/src/routes/channel/mapper.rs @@ -1,13 +1,20 @@ -use crate::models::{channel, channel_role_permission, channel_user_permission}; -use crate::repositories::types::ChannelFilter; use crate::domain::dto::channel::{ ChannelQueryParams, ChannelResponse, ChannelRolePermissionResponse, ChannelUserPermissionResponse, CreateChannelRequest, UpdateChannelRequest, }; +use crate::models::{channel, channel_role_permission, channel_user_permission}; +use crate::repositories::types::ChannelFilter; use sea_orm::Set; use uuid::Uuid; pub fn channel_model_to_channel_response(model: channel::Model) -> ChannelResponse { + channel_model_to_channel_response_with_permission(model, None) +} + +pub fn channel_model_to_channel_response_with_permission( + model: channel::Model, + permission: Option, +) -> ChannelResponse { ChannelResponse { id: model.id, server_id: model.server_id, @@ -16,6 +23,7 @@ pub fn channel_model_to_channel_response(model: channel::Model) -> ChannelRespon name: model.name, created_at: model.created_at, updated_at: model.updated_at, + permission, } } diff --git a/src/routes/server/handlers.rs b/src/routes/server/handlers.rs index 03ff12f..1449ca4 100644 --- a/src/routes/server/handlers.rs +++ b/src/routes/server/handlers.rs @@ -3,9 +3,8 @@ use crate::domain::dto::server::{ CreateServerRequest, ServerResponse, ServerRolePermissionResponse, ServerTreeResponse, ServerUserPermissionResponse, SetServerPermissionRequest, UpdateServerRequest, }; -use crate::http::context::Superuser; +use crate::http::context::{CurrentUser, Superuser}; use crate::http::error::HTTPError; -use crate::repositories::types::ChannelFilter; use crate::routes::server::mapper; use axum::{ Json, @@ -384,6 +383,7 @@ pub async fn remove_role_permission( tag = "Servers" )] pub async fn get_tree( + user: CurrentUser, State(state): State, Path(server_id): Path, ) -> Result, HTTPError> { @@ -395,20 +395,15 @@ pub async fn get_tree( .await? .ok_or(HTTPError::NotFound)?; - // Exécution parallèle des 3 requêtes via leurs repositories respectifs - let (orders, channels, categories) = tokio::try_join!( - state - .repositories - .server_item_order - .get_by_server(server_id), - state.repositories.channel.filter(ChannelFilter { - server_id: Some(server_id) - }), - state.repositories.category.get_by_server(server_id), - )?; + let tree = state + .repositories + .server_tree + .get_for_user(server_id, user.id) + .await?; - // Assemblage du ServerTreeResponse via le mapper - let tree = mapper::build_server_tree(orders, channels, categories); - - Ok(Json(tree)) + Ok(Json(mapper::build_server_tree( + tree.orders, + tree.channels, + tree.categories, + ))) } diff --git a/src/routes/server/mapper.rs b/src/routes/server/mapper.rs index ad5a908..26e21d3 100644 --- a/src/routes/server/mapper.rs +++ b/src/routes/server/mapper.rs @@ -2,11 +2,10 @@ use crate::domain::dto::server::{ CreateServerRequest, ServerExplorerItemResponse, ServerResponse, ServerRolePermissionResponse, ServerTreeResponse, ServerUserPermissionResponse, UpdateServerRequest, }; -use crate::models::{ - category, channel, server, server_item_order, server_role_permission, server_user_permission, -}; -use crate::routes::category::mapper::category_model_to_category_response; -use crate::routes::channel::mapper::channel_model_to_channel_response; +use crate::models::{server, server_item_order, server_role_permission, server_user_permission}; +use crate::repositories::types::{CategoryWithPermissions, ChannelWithPermissions}; +use crate::routes::category::mapper::category_model_to_category_response_with_permission; +use crate::routes::channel::mapper::channel_model_to_channel_response_with_permission; use sea_orm::Set; use std::collections::HashMap; use uuid::Uuid; @@ -65,44 +64,53 @@ pub fn server_role_permission_to_response( pub fn build_server_tree( orders: Vec, - channels: Vec, - categories: Vec, + channels: Vec, + categories: Vec, ) -> ServerTreeResponse { - // Map d'accès rapide : resource_id -> order_key let order_map: HashMap = orders .into_iter() - .map(|o| (o.resource_id, o.order_key)) + .map(|order| (order.resource_id, order.order_key)) .collect(); - // Regrouper les canaux par catégorie ou orphelins - let mut category_channels: HashMap> = HashMap::new(); - let mut orphan_channels: Vec = Vec::new(); + let mut category_channels: HashMap> = HashMap::new(); + let mut orphan_channels: Vec = Vec::new(); - for chan in channels { - if let Some(cat_id) = chan.category_id { - category_channels.entry(cat_id).or_default().push(chan); + for channel_with_permissions in channels { + if let Some(category_id) = channel_with_permissions.channel.category_id { + category_channels + .entry(category_id) + .or_default() + .push(channel_with_permissions); } else { - orphan_channels.push(chan); + orphan_channels.push(channel_with_permissions); } } // Trier les canaux dans chaque catégorie par leur order_key for chans in category_channels.values_mut() { - chans.sort_by_key(|c| order_map.get(&c.id).copied().unwrap_or(i64::MAX)); + chans.sort_by_key(|c| order_map.get(&c.channel.id).copied().unwrap_or(i64::MAX)); } // Préparer les éléments racine (catégories & canaux orphelins) avec leur order_key let mut root_items: Vec<(ServerExplorerItemResponse, i64)> = Vec::new(); - for cat in categories { - let cat_id = cat.id; + for cat_with_perm in categories { + let cat_id = cat_with_perm.category.id; let order_key = order_map.get(&cat_id).copied().unwrap_or(i64::MAX); let chans = category_channels.remove(&cat_id).unwrap_or_default(); - let cat_response = category_model_to_category_response(cat); + let cat_perm_bits = cat_with_perm.permissions.map(|p| p.bits()).unwrap_or(0); + let cat_response = category_model_to_category_response_with_permission( + cat_with_perm.category, + Some(cat_perm_bits), + ); + let chan_responses = chans .into_iter() - .map(channel_model_to_channel_response) + .map(|c| { + let chan_perm_bits = c.permissions.map(|p| p.bits()).unwrap_or(0); + channel_model_to_channel_response_with_permission(c.channel, Some(chan_perm_bits)) + }) .collect(); root_items.push(( @@ -111,9 +119,16 @@ pub fn build_server_tree( )); } - for chan in orphan_channels { - let order_key = order_map.get(&chan.id).copied().unwrap_or(i64::MAX); - let chan_response = channel_model_to_channel_response(chan); + for chan_with_perm in orphan_channels { + let order_key = order_map + .get(&chan_with_perm.channel.id) + .copied() + .unwrap_or(i64::MAX); + let chan_perm_bits = chan_with_perm.permissions.map(|p| p.bits()).unwrap_or(0); + let chan_response = channel_model_to_channel_response_with_permission( + chan_with_perm.channel, + Some(chan_perm_bits), + ); root_items.push(( ServerExplorerItemResponse::Channel(chan_response), order_key, diff --git a/src/services/channel.rs b/src/services/channel.rs index af91ffe..39c88b4 100644 --- a/src/services/channel.rs +++ b/src/services/channel.rs @@ -1,9 +1,11 @@ -use crate::repositories::Repositories; -use crate::services::ServicesContext; use crate::domain::dto::channel::{CreateChannelRequest, UpdateChannelRequest}; -use crate::models::channel; -use event_bus::Scope; -use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, QueryOrder, TransactionTrait, Set, PaginatorTrait}; +use crate::models::{channel, role}; +use crate::permissions::PermissionSet; +use crate::services::ServicesContext; +use crate::services::permission::PermissionService; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, Set, TransactionTrait, +}; use std::sync::Arc; use uuid::Uuid; @@ -24,47 +26,80 @@ impl ChannelService { let db = &self.service_context.repositories.server.context.db; let event_bus = &self.service_context.event_bus; - // Start transaction - let txn = db.begin().await?; + let channel = db + .transaction::<_, channel::Model, anyhow::Error>(|txn| { + Box::pin(async move { + // 1. Insertion du canal + let active_model = channel::ActiveModel { + server_id: Set(payload.server_id), + category_id: Set(payload.category_id), + channel_type: Set(payload.channel_type), + name: Set(payload.name), + ..Default::default() + }; - // 1. Insert channel within transaction - let active_model = channel::ActiveModel { - server_id: Set(payload.server_id), - category_id: Set(payload.category_id), - channel_type: Set(payload.channel_type), - name: Set(payload.name), - ..Default::default() - }; + let channel = active_model.insert(txn).await?; - let channel = active_model.insert(&txn).await?; + // 2. Si server_id est présent, enregistrement de l'ordre d'affichage + if let Some(server_id) = payload.server_id { + let max_order: Option = + crate::models::server_item_order::Entity::find() + .filter( + crate::models::server_item_order::Column::ServerId + .eq(server_id), + ) + .select_only() + .column_as( + crate::models::server_item_order::Column::OrderKey.max(), + "max_key", + ) + .into_tuple::>() + .one(txn) + .await? + .flatten(); - // 2. If server_id is present, insert into server_item_order within transaction - if let Some(server_id) = payload.server_id { - // Get max order key or determine order key - let max_order: Option = crate::models::server_item_order::Entity::find() - .filter(crate::models::server_item_order::Column::ServerId.eq(server_id)) - .select_only() - .column_as(crate::models::server_item_order::Column::OrderKey.max(), "max_key") - .into_tuple::>() - .one(&txn) - .await? - .flatten(); + let next_order = max_order.unwrap_or(0) + 1; - let next_order = max_order.unwrap_or(0) + 1; + let order_item = crate::models::server_item_order::ActiveModel { + server_id: Set(server_id), + resource_id: Set(channel.id), + resource_type: Set( + crate::models::server_item_order::OrderedResourceType::Channel, + ), + parent_category_id: Set(payload.category_id), + order_key: Set(next_order), + ..Default::default() + }; + order_item.insert(txn).await?; - let order_item = crate::models::server_item_order::ActiveModel { - server_id: Set(server_id), - resource_id: Set(channel.id), - resource_type: Set(crate::models::server_item_order::OrderedResourceType::Channel), - parent_category_id: Set(payload.category_id), - order_key: Set(next_order), - ..Default::default() - }; - order_item.insert(&txn).await?; - } + // 3. Attribution des permissions par défaut au rôle par défaut + if let Some(default_role) = role::Entity::find() + .filter(role::Column::ServerId.eq(server_id)) + .filter(role::Column::IsDefault.eq(true)) + .one(txn) + .await? + { + let default_channel_permissions = + PermissionSet::DEFAULT.channel.bits() as i64; - // Commit transaction - txn.commit().await?; + let role_perm = crate::models::channel_role_permission::ActiveModel { + channel_id: Set(channel.id), + role_id: Set(default_role.id), + permissions: Set(default_channel_permissions), + ..Default::default() + }; + role_perm.insert(txn).await?; + } + + // 4. Refresh des permissions (Global pour l'instant) + // TODO: Cibler/focaliser le spectre du recalcul (ex: uniquement les membres impactés ou le canal spécifique) + PermissionService::sync_server_with_db(txn, server_id).await?; + } + + Ok(channel) + }) + }) + .await?; // Post-commit event emission event_bus.emit("channel_created", channel.clone()); @@ -126,9 +161,7 @@ impl ChannelService { .exec(&txn) .await?; - let res = channel::Entity::delete_by_id(id) - .exec(&txn) - .await?; + let res = channel::Entity::delete_by_id(id).exec(&txn).await?; let deleted = res.rows_affected > 0; @@ -173,7 +206,10 @@ impl ChannelService { txn.commit().await?; - event_bus.emit("channel_user_permission_created", (channel_id, user_id, permissions)); + event_bus.emit( + "channel_user_permission_created", + (channel_id, user_id, permissions), + ); Ok(()) } @@ -233,7 +269,10 @@ impl ChannelService { txn.commit().await?; - event_bus.emit("channel_role_permission_created", (channel_id, role_id, permissions)); + event_bus.emit( + "channel_role_permission_created", + (channel_id, role_id, permissions), + ); Ok(()) } diff --git a/src/services/mod.rs b/src/services/mod.rs index 7584e85..8d2a44e 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,23 +1,25 @@ use crate::repositories::Repositories; -use crate::services::permission_sync::PermissionSyncService; -use crate::services::server_order::ServerOrderService; -use crate::services::channel::ChannelService; -use crate::services::server::ServerService; use crate::services::category::CategoryService; +use crate::services::channel::ChannelService; use crate::services::message::MessageService; -use crate::services::user::UserService; +use crate::services::permission::PermissionService; +use crate::services::permission_sync::PermissionSyncService; use crate::services::role::RoleService; +use crate::services::server::ServerService; +use crate::services::server_order::ServerOrderService; +use crate::services::user::UserService; use event_bus::EventBus; use std::sync::{Arc, OnceLock}; -pub mod permission_sync; -mod server_order; -pub mod channel; -pub mod server; pub mod category; +pub mod channel; pub mod message; -pub mod user; +mod permission; +pub mod permission_sync; pub mod role; +pub mod server; +mod server_order; +pub mod user; #[derive(Debug, Clone)] pub struct ServicesContext { @@ -36,6 +38,7 @@ pub struct Services { pub message: Arc, pub user: Arc, pub role: Arc, + pub permission: Arc, } impl Services { @@ -53,6 +56,7 @@ impl Services { let message = Arc::new(MessageService::new(service_context.clone())); let user = Arc::new(UserService::new(service_context.clone())); let role = Arc::new(RoleService::new(service_context.clone())); + let permission = Arc::new(PermissionService::new(service_context.clone())); let services = Self { permission_sync, @@ -63,6 +67,7 @@ impl Services { message, user, role, + permission, }; let _ = service_context.services.set(services.clone()); services diff --git a/src/services/permission.rs b/src/services/permission.rs new file mode 100644 index 0000000..0df518f --- /dev/null +++ b/src/services/permission.rs @@ -0,0 +1,438 @@ +use crate::models::computed_permission::PermissionScopeType; +use crate::models::{ + channel, channel_role_permission, channel_user_permission, computed_permission, role_user, + server_role_permission, server_user, server_user_permission, +}; +use crate::permissions::{ChannelPermission, ServerPermission}; +use crate::services::ServicesContext; +use sea_orm::{ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, QuerySelect, Set}; +use std::collections::HashMap; +use std::sync::Arc; +use uuid::Uuid; + +#[derive(Debug, Clone)] +pub struct PermissionService { + service_context: Arc, +} + +impl PermissionService { + pub fn new(service_context: Arc) -> Self { + Self { service_context } + } + + /// Recalcule de manière globale et optimisée `computed_permission` pour TOUS les utilisateurs + /// et TOUTES les ressources d'un serveur donné. + pub async fn sync_server(&self, server_id: Uuid) -> Result<(), anyhow::Error> { + let db = &self.service_context.repositories.server.context.db; + Self::sync_server_with_db(db, server_id).await + } + + /// Recalcule TOUTES les entrées `computed_permission` d'un utilisateur sur un serveur. + pub async fn sync_user(&self, user_id: Uuid, server_id: Uuid) -> Result<(), anyhow::Error> { + let db = &self.service_context.repositories.server.context.db; + Self::sync_user_with_db(db, user_id, server_id).await + } + + /// Recalcule le `computed_permission` pour UN utilisateur et UN canal spécifique. + pub async fn sync_channel_user_permission( + &self, + channel_id: Uuid, + user_id: Uuid, + ) -> Result<(), anyhow::Error> { + let db = &self.service_context.repositories.server.context.db; + Self::sync_channel_user_permission_with_db(db, channel_id, user_id).await + } +} + +impl PermissionService { + pub async fn sync_server_with_db(db: &C, server_id: Uuid) -> Result<(), anyhow::Error> + where + C: ConnectionTrait, + { + // 1. Charger tous les canaux du serveur + let channels = channel::Entity::find() + .filter(channel::Column::ServerId.eq(server_id)) + .all(db) + .await?; + + let channel_ids: Vec = channels.iter().map(|c| c.id).collect(); + + // 2. Charger tous les utilisateurs du serveur + let user_ids: Vec = server_user::Entity::find() + .filter(server_user::Column::ServerId.eq(server_id)) + .select_only() + .column(server_user::Column::UserId) + .into_tuple::() + .all(db) + .await?; + + if user_ids.is_empty() { + // Aucun membre, nettoyer simplement le cache de ce serveur + computed_permission::Entity::delete_many() + .filter(computed_permission::Column::ServerId.eq(server_id)) + .exec(db) + .await?; + return Ok(()); + } + + // 3. Charger en lot toutes les affectations de rôles des membres + let user_roles_models = role_user::Entity::find() + .filter(role_user::Column::UserId.is_in(user_ids.clone())) + .all(db) + .await?; + + let mut roles_by_user: HashMap> = HashMap::new(); + for ur in user_roles_models { + roles_by_user + .entry(ur.user_id) + .or_default() + .push(ur.role_id); + } + + // 4. Charger toutes les permissions serveur (Rôles & Utilisateurs) + let server_role_perms = server_role_permission::Entity::find() + .filter(server_role_permission::Column::ServerId.eq(server_id)) + .all(db) + .await?; + + let mut server_perm_by_role: HashMap = HashMap::new(); + for srp in server_role_perms { + server_perm_by_role.insert( + srp.role_id, + ServerPermission::from_bits_retain(srp.permissions as u64), + ); + } + + let server_user_perms = server_user_permission::Entity::find() + .filter(server_user_permission::Column::ServerId.eq(server_id)) + .filter(server_user_permission::Column::UserId.is_in(user_ids.clone())) + .all(db) + .await?; + + let mut server_perm_by_user: HashMap = HashMap::new(); + for sup in server_user_perms { + server_perm_by_user.insert( + sup.user_id, + ServerPermission::from_bits_retain(sup.permissions as u64), + ); + } + + // 5. Charger toutes les permissions de canaux (Rôles & Utilisateurs) + let channel_role_perms = if channel_ids.is_empty() { + Vec::new() + } else { + channel_role_permission::Entity::find() + .filter(channel_role_permission::Column::ChannelId.is_in(channel_ids.clone())) + .all(db) + .await? + }; + + let mut channel_role_perm_map: HashMap<(Uuid, Uuid), ChannelPermission> = HashMap::new(); + for crp in channel_role_perms { + channel_role_perm_map.insert( + (crp.channel_id, crp.role_id), + ChannelPermission::from_bits_retain(crp.permissions as u64), + ); + } + + let channel_user_perms = if channel_ids.is_empty() { + Vec::new() + } else { + channel_user_permission::Entity::find() + .filter(channel_user_permission::Column::ChannelId.is_in(channel_ids.clone())) + .filter(channel_user_permission::Column::UserId.is_in(user_ids.clone())) + .all(db) + .await? + }; + + let mut channel_user_perm_map: HashMap<(Uuid, Uuid), ChannelPermission> = HashMap::new(); + for cup in channel_user_perms { + channel_user_perm_map.insert( + (cup.channel_id, cup.user_id), + ChannelPermission::from_bits_retain(cup.permissions as u64), + ); + } + + // 6. Calcul en RAM pour l'ensemble des paires (Utilisateur x Ressource) + let mut to_insert: Vec = Vec::new(); + + for user_id in user_ids { + let user_roles = roles_by_user.get(&user_id); + + // A. Permission Serveur + let mut final_server_perm = ServerPermission::empty(); + if let Some(roles) = user_roles { + for role_id in roles { + if let Some(p) = server_perm_by_role.get(role_id) { + final_server_perm |= *p; + } + } + } + if let Some(p) = server_perm_by_user.get(&user_id) { + final_server_perm |= *p; + } + + to_insert.push(computed_permission::ActiveModel { + user_id: Set(user_id), + server_id: Set(server_id), + scope_type: Set(PermissionScopeType::Server), + resource_id: Set(server_id), + permissions: Set(final_server_perm.bits() as i64), + ..Default::default() + }); + + // B. Permissions par Canal + for channel in &channels { + let mut final_channel_perm = ChannelPermission::empty(); + + if let Some(roles) = user_roles { + for role_id in roles { + if let Some(p) = channel_role_perm_map.get(&(channel.id, *role_id)) { + final_channel_perm |= *p; + } + } + } + + if let Some(p) = channel_user_perm_map.get(&(channel.id, user_id)) { + final_channel_perm |= *p; + } + + to_insert.push(computed_permission::ActiveModel { + user_id: Set(user_id), + server_id: Set(server_id), + scope_type: Set(PermissionScopeType::Channel), + resource_id: Set(channel.id), + permissions: Set(final_channel_perm.bits() as i64), + ..Default::default() + }); + } + } + + // 7. Remplacement atomique complet pour le serveur en BDD + computed_permission::Entity::delete_many() + .filter(computed_permission::Column::ServerId.eq(server_id)) + .exec(db) + .await?; + + if !to_insert.is_empty() { + // Insertion par lots (chunking de 1000 pour éviter les limites de paramètres SQL) + for chunk in to_insert.chunks(1000) { + computed_permission::Entity::insert_many(chunk.to_vec()) + .exec(db) + .await?; + } + } + + Ok(()) + } + + pub async fn sync_user_with_db( + db: &C, + user_id: Uuid, + server_id: Uuid, + ) -> Result<(), anyhow::Error> + where + C: ConnectionTrait, + { + // 1. Rôles de l'utilisateur + let role_ids = role_user::Entity::find() + .filter(role_user::Column::UserId.eq(user_id)) + .select_only() + .column(role_user::Column::RoleId) + .into_tuple::() + .all(db) + .await?; + + // 2. Permissions serveur des rôles + let mut server_permissions = ServerPermission::empty(); + if !role_ids.is_empty() { + let role_permissions = server_role_permission::Entity::find() + .filter(server_role_permission::Column::ServerId.eq(server_id)) + .filter(server_role_permission::Column::RoleId.is_in(role_ids.clone())) + .all(db) + .await?; + + for permission in role_permissions { + server_permissions |= + ServerPermission::from_bits_retain(permission.permissions as u64); + } + } + + // 3. Permissions serveur directes + if let Some(permission) = server_user_permission::Entity::find() + .filter(server_user_permission::Column::ServerId.eq(server_id)) + .filter(server_user_permission::Column::UserId.eq(user_id)) + .one(db) + .await? + { + server_permissions |= ServerPermission::from_bits_retain(permission.permissions as u64); + } + + // 4. Canaux du serveur + let channels = channel::Entity::find() + .filter(channel::Column::ServerId.eq(server_id)) + .all(db) + .await?; + + let channel_ids: Vec = channels.iter().map(|c| c.id).collect(); + + // 5. Permissions de rôles pour les canaux + let role_channel_permissions = if role_ids.is_empty() || channel_ids.is_empty() { + Vec::new() + } else { + channel_role_permission::Entity::find() + .filter(channel_role_permission::Column::ChannelId.is_in(channel_ids.clone())) + .filter(channel_role_permission::Column::RoleId.is_in(role_ids)) + .all(db) + .await? + }; + + let mut permissions_by_channel: HashMap = HashMap::new(); + for permission in role_channel_permissions { + permissions_by_channel + .entry(permission.channel_id) + .or_default() + .insert(ChannelPermission::from_bits_retain( + permission.permissions as u64, + )); + } + + // 6. Permissions directes utilisateur pour les canaux + let user_channel_permissions = if channel_ids.is_empty() { + Vec::new() + } else { + channel_user_permission::Entity::find() + .filter(channel_user_permission::Column::UserId.eq(user_id)) + .filter(channel_user_permission::Column::ChannelId.is_in(channel_ids)) + .all(db) + .await? + }; + + for permission in user_channel_permissions { + permissions_by_channel + .entry(permission.channel_id) + .or_default() + .insert(ChannelPermission::from_bits_retain( + permission.permissions as u64, + )); + } + + // 7. Modèles à insérer + let mut computed_permissions = Vec::with_capacity(channels.len().saturating_add(1)); + + computed_permissions.push(computed_permission::ActiveModel { + user_id: Set(user_id), + server_id: Set(server_id), + scope_type: Set(PermissionScopeType::Server), + resource_id: Set(server_id), + permissions: Set(server_permissions.bits() as i64), + ..Default::default() + }); + + for channel in channels { + let channel_permissions = permissions_by_channel + .remove(&channel.id) + .unwrap_or_else(ChannelPermission::empty); + + computed_permissions.push(computed_permission::ActiveModel { + user_id: Set(user_id), + server_id: Set(server_id), + scope_type: Set(PermissionScopeType::Channel), + resource_id: Set(channel.id), + permissions: Set(channel_permissions.bits() as i64), + ..Default::default() + }); + } + + // 8. Remplacement atomique BDD + computed_permission::Entity::delete_many() + .filter(computed_permission::Column::UserId.eq(user_id)) + .filter(computed_permission::Column::ServerId.eq(server_id)) + .exec(db) + .await?; + + if !computed_permissions.is_empty() { + computed_permission::Entity::insert_many(computed_permissions) + .exec(db) + .await?; + } + + Ok(()) + } + + pub async fn sync_channel_user_permission_with_db( + db: &C, + channel_id: Uuid, + user_id: Uuid, + ) -> Result<(), anyhow::Error> + where + C: ConnectionTrait, + { + // 1. Récupérer le canal pour connaître son server_id + let channel = channel::Entity::find_by_id(channel_id) + .one(db) + .await? + .ok_or_else(|| anyhow::anyhow!("Canal non trouvé"))?; + + let server_id = match channel.server_id { + Some(sid) => sid, + None => return Ok(()), // Canal DM ou sans serveur + }; + + // 2. Rôles de l'utilisateur sur le serveur + let role_ids = role_user::Entity::find() + .filter(role_user::Column::UserId.eq(user_id)) + .select_only() + .column(role_user::Column::RoleId) + .into_tuple::() + .all(db) + .await?; + + // 3. Permissions cumulées des rôles sur ce canal + let mut computed = ChannelPermission::empty(); + + if !role_ids.is_empty() { + let role_perms = channel_role_permission::Entity::find() + .filter(channel_role_permission::Column::ChannelId.eq(channel_id)) + .filter(channel_role_permission::Column::RoleId.is_in(role_ids)) + .all(db) + .await?; + + for p in role_perms { + computed |= ChannelPermission::from_bits_retain(p.permissions as u64); + } + } + + // 4. Override direct utilisateur sur le canal + if let Some(user_perm) = channel_user_permission::Entity::find() + .filter(channel_user_permission::Column::ChannelId.eq(channel_id)) + .filter(channel_user_permission::Column::UserId.eq(user_id)) + .one(db) + .await? + { + computed |= ChannelPermission::from_bits_retain(user_perm.permissions as u64); + } + + // 5. Suppression & insertion atomique de l'entrée computed_permission + computed_permission::Entity::delete_many() + .filter(computed_permission::Column::UserId.eq(user_id)) + .filter(computed_permission::Column::ResourceId.eq(channel_id)) + .exec(db) + .await?; + + let active_model = computed_permission::ActiveModel { + user_id: Set(user_id), + server_id: Set(server_id), + scope_type: Set(PermissionScopeType::Channel), + resource_id: Set(channel_id), + permissions: Set(computed.bits() as i64), + ..Default::default() + }; + + computed_permission::Entity::insert(active_model) + .exec(db) + .await?; + + Ok(()) + } +}