This commit is contained in:
2026-08-01 14:11:19 +02:00
parent 22b0bc36bb
commit b946cfd866
13 changed files with 689 additions and 152 deletions
+84 -45
View File
@@ -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<i64> =
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::<Option<i64>>()
.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<i64> = 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::<Option<i64>>()
.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(())
}
+15 -10
View File
@@ -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<MessageService>,
pub user: Arc<UserService>,
pub role: Arc<RoleService>,
pub permission: Arc<PermissionService>,
}
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
+438
View File
@@ -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<ServicesContext>,
}
impl PermissionService {
pub fn new(service_context: Arc<ServicesContext>) -> 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<C>(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<Uuid> = channels.iter().map(|c| c.id).collect();
// 2. Charger tous les utilisateurs du serveur
let user_ids: Vec<Uuid> = server_user::Entity::find()
.filter(server_user::Column::ServerId.eq(server_id))
.select_only()
.column(server_user::Column::UserId)
.into_tuple::<Uuid>()
.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<Uuid, Vec<Uuid>> = 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<Uuid, ServerPermission> = 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<Uuid, ServerPermission> = 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<computed_permission::ActiveModel> = 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<C>(
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::<Uuid>()
.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<Uuid> = 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<Uuid, ChannelPermission> = 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<C>(
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::<Uuid>()
.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(())
}
}