Init
This commit is contained in:
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -1580,6 +1580,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range-header"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
@@ -3731,6 +3737,19 @@ dependencies = [
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.8"
|
||||
@@ -3806,12 +3825,19 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
|
||||
@@ -44,7 +44,7 @@ utoipa-scalar = { version = "0.3", features = ["axum"] }
|
||||
utoipa-swagger-ui = { version = "9.0", features = ["axum"] }
|
||||
utoipa-axum = "0.2"
|
||||
tower = { version = "0.5", features = ["util"] }
|
||||
tower-http = { version = "0.6", features = ["trace", "cors", "timeout", "catch-panic"] }
|
||||
tower-http = { version = "0.6", features = ["trace", "cors", "timeout", "catch-panic", "fs"] }
|
||||
|
||||
# UDP
|
||||
socket2 = "0.6"
|
||||
|
||||
@@ -30,6 +30,11 @@ aux champs de l'utilisateur (ex: `user.is_superuser`) directement comme s'il s'a
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
- **API (REST & WebSocket) :** `/swagger-ui` ou `/scalar`
|
||||
- La documentation du WebSocket est incluse dans l'OpenAPI via une route descriptive `/handler/ws/`.
|
||||
|
||||
## TODO
|
||||
|
||||
Terminer le système d'authentification (login, changement de mot de passe...)
|
||||
|
||||
@@ -27,6 +27,12 @@ impl MigrationTrait for Migration {
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("default_permissions"))
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
@@ -114,6 +120,11 @@ impl MigrationTrait for Migration {
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("default_permissions"))
|
||||
.big_integer()
|
||||
.null(),
|
||||
)
|
||||
// Indexes créés après
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
@@ -299,6 +310,18 @@ impl MigrationTrait for Migration {
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("is_owner"))
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("permissions"))
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
// Indexes créés après
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
|
||||
@@ -9,7 +9,7 @@ pub struct GroupResponse {
|
||||
pub id: Uuid,
|
||||
pub server_id: Uuid,
|
||||
pub name: String,
|
||||
pub permissions: i64,
|
||||
pub permissions: u64,
|
||||
pub is_default: bool,
|
||||
pub created_at: String,
|
||||
}
|
||||
@@ -31,7 +31,7 @@ impl From<group::Model> for GroupResponse {
|
||||
pub struct CreateGroupRequest {
|
||||
pub server_id: Uuid,
|
||||
pub name: String,
|
||||
pub permissions: i64,
|
||||
pub permissions: u64,
|
||||
}
|
||||
|
||||
impl CreateGroupRequest {
|
||||
|
||||
@@ -32,6 +32,7 @@ pub struct Model {
|
||||
pub name: Option<String>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub default_permissions: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct Model {
|
||||
pub channel_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub role: String,
|
||||
pub permissions: i64,
|
||||
pub permissions: u64,
|
||||
pub joined_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ pub struct Model {
|
||||
pub id: Uuid,
|
||||
pub server_id: Uuid,
|
||||
pub name: String,
|
||||
pub permissions: i64,
|
||||
pub permissions: u64,
|
||||
pub is_default: bool,
|
||||
pub created_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ pub struct Model {
|
||||
pub password: Option<String>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub default_permissions: u64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -15,6 +15,8 @@ pub struct Model {
|
||||
pub joined_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub is_admin: bool,
|
||||
pub is_owner: bool,
|
||||
pub permissions: u64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -97,23 +97,12 @@ pub async fn category_create(
|
||||
user: CurrentUser,
|
||||
Json(payload): Json<CreateCategoryRequest>,
|
||||
) -> Result<Json<CategoryResponse>, HTTPError> {
|
||||
let is_admin = app_state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, payload.server_id)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
|
||||
let has_permission = if is_admin || is_superuser {
|
||||
true
|
||||
} else {
|
||||
let ctx = app_state
|
||||
let has_permission = user.is_superuser
|
||||
|| app_state
|
||||
.repositories
|
||||
.permission
|
||||
.load_for_server_user(user.id, payload.server_id)
|
||||
.server
|
||||
.has_perm(user.id, payload.server_id, Permission::ManageServer)
|
||||
.await?;
|
||||
ctx.has(Permission::ManageServer, None)
|
||||
};
|
||||
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
@@ -149,23 +138,12 @@ pub async fn category_update(
|
||||
.await?
|
||||
.ok_or_else(|| HTTPError::NotFound)?;
|
||||
|
||||
let is_admin = app_state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, category.server_id)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
|
||||
let has_permission = if is_admin || is_superuser {
|
||||
true
|
||||
} else {
|
||||
let ctx = app_state
|
||||
let has_permission = user.is_superuser
|
||||
|| app_state
|
||||
.repositories
|
||||
.permission
|
||||
.load_for_server_user(user.id, category.server_id)
|
||||
.server
|
||||
.has_perm(user.id, category.server_id, Permission::ManageServer)
|
||||
.await?;
|
||||
ctx.has(Permission::ManageServer, None)
|
||||
};
|
||||
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
@@ -203,23 +181,12 @@ pub async fn category_delete(
|
||||
.await?
|
||||
.ok_or_else(|| HTTPError::NotFound)?;
|
||||
|
||||
let is_admin = app_state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, category.server_id)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
|
||||
let has_permission = if is_admin || is_superuser {
|
||||
true
|
||||
} else {
|
||||
let ctx = app_state
|
||||
let has_permission = user.is_superuser
|
||||
|| app_state
|
||||
.repositories
|
||||
.permission
|
||||
.load_for_server_user(user.id, category.server_id)
|
||||
.server
|
||||
.has_perm(user.id, category.server_id, Permission::ManageServer)
|
||||
.await?;
|
||||
ctx.has(Permission::ManageServer, None)
|
||||
};
|
||||
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
|
||||
@@ -79,23 +79,12 @@ pub async fn channel_create(
|
||||
Json(dto): Json<CreateChannelRequest>,
|
||||
) -> Result<Json<ChannelResponse>, HTTPError> {
|
||||
if let Some(server_id) = dto.server_id {
|
||||
let is_admin = app_state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, server_id)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
|
||||
let has_permission = if is_admin || is_superuser {
|
||||
true
|
||||
} else {
|
||||
let ctx = app_state
|
||||
let has_permission = user.is_superuser
|
||||
|| app_state
|
||||
.repositories
|
||||
.permission
|
||||
.load_for_server_user(user.id, server_id)
|
||||
.server
|
||||
.has_perm(user.id, server_id, Permission::ManageServer)
|
||||
.await?;
|
||||
ctx.has(Permission::ManageServer, None)
|
||||
};
|
||||
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
@@ -133,23 +122,17 @@ pub async fn channel_update(
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
if let Some(server_id) = channel.server_id {
|
||||
let is_admin = app_state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, server_id)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
|
||||
let has_permission = if is_admin || is_superuser {
|
||||
true
|
||||
} else {
|
||||
let ctx = app_state
|
||||
let has_permission = user.is_superuser
|
||||
|| app_state
|
||||
.repositories
|
||||
.permission
|
||||
.load_for_server_user(user.id, server_id)
|
||||
.channel
|
||||
.has_perm(user.id, id, Permission::ManageChannel)
|
||||
.await?
|
||||
|| app_state
|
||||
.repositories
|
||||
.server
|
||||
.has_perm(user.id, server_id, Permission::ManageServer)
|
||||
.await?;
|
||||
ctx.has(Permission::ManageChannel, Some(id)) || ctx.has(Permission::ManageServer, None)
|
||||
};
|
||||
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
@@ -189,23 +172,17 @@ pub async fn channel_delete(
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
if let Some(server_id) = channel.server_id {
|
||||
let is_admin = app_state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, server_id)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
|
||||
let has_permission = if is_admin || is_superuser {
|
||||
true
|
||||
} else {
|
||||
let ctx = app_state
|
||||
let has_permission = user.is_superuser
|
||||
|| app_state
|
||||
.repositories
|
||||
.permission
|
||||
.load_for_server_user(user.id, server_id)
|
||||
.channel
|
||||
.has_perm(user.id, id, Permission::ManageChannel)
|
||||
.await?
|
||||
|| app_state
|
||||
.repositories
|
||||
.server
|
||||
.has_perm(user.id, server_id, Permission::ManageServer)
|
||||
.await?;
|
||||
ctx.has(Permission::ManageChannel, Some(id)) || ctx.has(Permission::ManageServer, None)
|
||||
};
|
||||
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
|
||||
@@ -87,23 +87,12 @@ pub async fn group_create(
|
||||
user: CurrentUser,
|
||||
Json(payload): Json<CreateGroupRequest>,
|
||||
) -> Result<Json<GroupResponse>, HTTPError> {
|
||||
let is_admin = state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, payload.server_id)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
|
||||
let has_permission = if is_admin || is_superuser {
|
||||
true
|
||||
} else {
|
||||
let ctx = state
|
||||
let has_permission = user.is_superuser
|
||||
|| state
|
||||
.repositories
|
||||
.permission
|
||||
.load_for_server_user(user.id, payload.server_id)
|
||||
.server
|
||||
.has_perm(user.id, payload.server_id, Permission::ManageRoles)
|
||||
.await?;
|
||||
ctx.has(Permission::ManageRoles, None)
|
||||
};
|
||||
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
@@ -141,23 +130,12 @@ pub async fn group_update(
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
let is_admin = state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, group.server_id)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
|
||||
let has_permission = if is_admin || is_superuser {
|
||||
true
|
||||
} else {
|
||||
let ctx = state
|
||||
let has_permission = user.is_superuser
|
||||
|| state
|
||||
.repositories
|
||||
.permission
|
||||
.load_for_server_user(user.id, group.server_id)
|
||||
.server
|
||||
.has_perm(user.id, group.server_id, Permission::ManageRoles)
|
||||
.await?;
|
||||
ctx.has(Permission::ManageRoles, None)
|
||||
};
|
||||
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
@@ -197,23 +175,12 @@ pub async fn group_delete(
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
let is_admin = state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, group.server_id)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
|
||||
let has_permission = if is_admin || is_superuser {
|
||||
true
|
||||
} else {
|
||||
let ctx = state
|
||||
let has_permission = user.is_superuser
|
||||
|| state
|
||||
.repositories
|
||||
.permission
|
||||
.load_for_server_user(user.id, group.server_id)
|
||||
.server
|
||||
.has_perm(user.id, group.server_id, Permission::ManageRoles)
|
||||
.await?;
|
||||
ctx.has(Permission::ManageRoles, None)
|
||||
};
|
||||
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
|
||||
@@ -88,22 +88,15 @@ pub async fn message_create(
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
if let Some(server_id) = channel.server_id {
|
||||
let is_admin = app_state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, server_id)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
|
||||
if !is_admin && !is_superuser {
|
||||
let ctx = app_state
|
||||
let has_permission = user.is_superuser
|
||||
|| app_state
|
||||
.repositories
|
||||
.permission
|
||||
.load_for_server_user(user.id, server_id)
|
||||
.channel
|
||||
.has_perm(user.id, channel.id, Permission::SendMessage)
|
||||
.await?;
|
||||
if !ctx.has(Permission::SendMessage, Some(channel.id)) {
|
||||
return Err(HTTPError::Forbidden);
|
||||
}
|
||||
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,25 +172,14 @@ pub async fn message_delete(
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
if let Some(server_id) = channel.server_id {
|
||||
let is_admin = app_state
|
||||
let has_permission = user.is_superuser
|
||||
|| app_state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, server_id)
|
||||
.channel
|
||||
.has_perm(user.id, channel.id, Permission::DeleteOthersMessage)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
|
||||
if !is_admin && !is_superuser {
|
||||
let ctx = app_state
|
||||
.repositories
|
||||
.permission
|
||||
.load_for_server_user(user.id, server_id)
|
||||
.await?;
|
||||
if !ctx.has(Permission::DeleteOthersMessage, Some(channel.id)) {
|
||||
return Err(HTTPError::Forbidden);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::interfaces::http::dto::{
|
||||
auth as auth_dto, category as category_dto, channel as channel_dto, message as message_dto,
|
||||
category as category_dto, channel as channel_dto, message as message_dto,
|
||||
server as server_dto, user as user_dto,
|
||||
};
|
||||
use crate::network::http::web::api::{auth, category, channel, message, server, user};
|
||||
use crate::network::http::web::ws_handler;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
@@ -36,6 +37,7 @@ use utoipa::OpenApi;
|
||||
user::get_me,
|
||||
user::user_list,
|
||||
user::user_detail,
|
||||
ws_handler::ws_doc,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
|
||||
@@ -5,10 +5,11 @@ use crate::interfaces::http::dto::server::{
|
||||
use crate::models::server;
|
||||
use crate::network::http::context::CurrentUser;
|
||||
use crate::network::http::{AppRouter, HTTPError};
|
||||
use crate::utils::permissions::Permission;
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::get;
|
||||
use axum::Json;
|
||||
use sea_orm::IntoActiveModel;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -107,14 +108,14 @@ pub async fn server_update(
|
||||
Path(id): Path<Uuid>,
|
||||
Json(serializer): Json<CreateServerRequest>,
|
||||
) -> Result<Json<ServerResponse>, HTTPError> {
|
||||
let is_admin = state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, id)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
let has_permission = user.is_superuser
|
||||
|| state
|
||||
.repositories
|
||||
.server
|
||||
.has_perm(user.id, id, Permission::ManageServer)
|
||||
.await?;
|
||||
|
||||
if !is_admin && !is_superuser {
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
}
|
||||
|
||||
@@ -152,14 +153,14 @@ pub async fn server_delete(
|
||||
user: CurrentUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, HTTPError> {
|
||||
let is_admin = state
|
||||
.repositories
|
||||
.permission
|
||||
.is_server_admin(user.id, id)
|
||||
.await?;
|
||||
let is_superuser = user.is_superuser;
|
||||
let has_permission = user.is_superuser
|
||||
|| state
|
||||
.repositories
|
||||
.server
|
||||
.has_perm(user.id, id, Permission::ManageServer)
|
||||
.await?;
|
||||
|
||||
if !is_admin && !is_superuser {
|
||||
if !has_permission {
|
||||
return Err(HTTPError::Forbidden);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,45 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use axum::extract::{State, WebSocketUpgrade};
|
||||
use axum::extract::ws::{Message, WebSocket};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::{delete, get, post, put};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use parking_lot::RwLock;
|
||||
use tokio::sync::mpsc;
|
||||
use uuid::Uuid;
|
||||
use crate::app::AppState;
|
||||
use crate::hub::Client;
|
||||
use crate::network::http::AppRouter;
|
||||
use axum::extract::ws::{Message, WebSocket};
|
||||
use axum::extract::{State, WebSocketUpgrade};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::get;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use tokio::sync::mpsc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn setup_route() -> AppRouter {
|
||||
AppRouter::new()
|
||||
.route("/ws/", get(ws_handler))
|
||||
AppRouter::new().route("/ws/", get(ws_handler))
|
||||
}
|
||||
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(app_state): State<AppState>
|
||||
) -> impl IntoResponse {
|
||||
/// WebSocket documentation placeholder.
|
||||
///
|
||||
/// This is not a real HTTP endpoint, but it documents the WebSocket behavior.
|
||||
///
|
||||
/// **Connection:** `GET /handler/ws/`
|
||||
///
|
||||
/// **Protocol:** JSON messages over WebSocket.
|
||||
///
|
||||
/// ### Client -> Server
|
||||
/// - `SendMessage`: Send a JSON object matching `CreateMessageRequest`.
|
||||
///
|
||||
/// ### Server -> Client
|
||||
/// - `MessageReceived`: Receive a JSON object matching `MessageResponse`.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/handler/ws/",
|
||||
responses(
|
||||
(status = 101, description = "Switching Protocols to WebSocket"),
|
||||
(status = 401, description = "Unauthorized (Invalid JWT)")
|
||||
),
|
||||
security(
|
||||
("jwt" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn ws_doc() {}
|
||||
|
||||
async fn ws_handler(ws: WebSocketUpgrade, State(app_state): State<AppState>) -> impl IntoResponse {
|
||||
// todo --- 1. VÉRIFICATION AVANT UPGRADE ---
|
||||
// C'est ici qu'on vérifierait le JWT par exemple.
|
||||
// Si ça échoue, on peut retourner une erreur HTTP direct.
|
||||
@@ -50,7 +69,7 @@ async fn handle_socket(socket: WebSocket, user_id: Uuid, app_state: AppState) {
|
||||
|
||||
client.on_connect().await;
|
||||
|
||||
// 3. Tâche d'ENVOI : On écoute la boîte aux lettres et on pousse vers le navigateur
|
||||
// 3. Tâche d'ENVOI : On écoute la boîte aux lettres et on pousse vers le client
|
||||
let mut send_task = tokio::spawn(async move {
|
||||
while let Some(msg) = rx.recv().await {
|
||||
if ws_sender.send(msg).await.is_err() {
|
||||
@@ -81,4 +100,4 @@ async fn handle_socket(socket: WebSocket, user_id: Uuid, app_state: AppState) {
|
||||
client.on_disconnect().await;
|
||||
app_state.clients.remove_client(connection_id);
|
||||
println!("Session terminée pour le client {}", connection_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait};
|
||||
use crate::models::channel;
|
||||
use crate::repositories::permission::PermissionRepository;
|
||||
use crate::repositories::RepositoryContext;
|
||||
use crate::utils::permissions::Permission;
|
||||
use sea_orm::{ActiveModelTrait, DbErr, EntityTrait};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ChannelRepository {
|
||||
pub context: Arc<RepositoryContext>
|
||||
pub context: Arc<RepositoryContext>,
|
||||
}
|
||||
|
||||
impl ChannelRepository {
|
||||
@@ -26,8 +28,23 @@ impl ChannelRepository {
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||
channel::Entity::delete_by_id(id).exec(&self.context.db).await?;
|
||||
channel::Entity::delete_by_id(id)
|
||||
.exec(&self.context.db)
|
||||
.await?;
|
||||
self.context.events.emit("channel_deleted", id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn has_perm(
|
||||
&self,
|
||||
user_id: uuid::Uuid,
|
||||
channel_id: uuid::Uuid,
|
||||
permission: Permission,
|
||||
) -> Result<bool, DbErr> {
|
||||
let repo = PermissionRepository {
|
||||
context: self.context.clone(),
|
||||
};
|
||||
let stack = repo.get_channel_stack(user_id, channel_id).await?;
|
||||
Ok(stack.has(permission))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::models::{channel_user, group, group_member, server_user};
|
||||
use crate::models::{channel, channel_user, group, group_member, server, server_user};
|
||||
use crate::repositories::RepositoryContext;
|
||||
use crate::utils::permissions::PermissionContext;
|
||||
use crate::utils::permissions::PermissionStack;
|
||||
use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
@@ -11,72 +11,100 @@ pub struct PermissionRepository {
|
||||
}
|
||||
|
||||
impl PermissionRepository {
|
||||
pub async fn is_server_admin(&self, user_id: Uuid, server_id: Uuid) -> Result<bool, DbErr> {
|
||||
let res = server_user::Entity::find()
|
||||
.filter(server_user::Column::UserId.eq(user_id))
|
||||
.filter(server_user::Column::ServerId.eq(server_id))
|
||||
.one(&self.context.db)
|
||||
.await?;
|
||||
|
||||
Ok(res.map(|su| su.is_admin).unwrap_or(false))
|
||||
}
|
||||
|
||||
pub async fn load_for_user(&self, user_id: Uuid) -> Result<PermissionContext, DbErr> {
|
||||
// Requête 1 : JOIN group_member → group filtre group_member.user_id = user_id
|
||||
// collecter les group.permissions: i64 dans un Vec<i64>
|
||||
let group_masks: Vec<i64> = group::Entity::find()
|
||||
.inner_join(group_member::Entity)
|
||||
.filter(group_member::Column::UserId.eq(user_id))
|
||||
.select_only()
|
||||
.column(group::Column::Permissions)
|
||||
.into_tuple()
|
||||
.all(&self.context.db)
|
||||
.await?;
|
||||
|
||||
// Requête 2 : SELECT channel_id, permissions FROM channel_user WHERE user_id = user_id
|
||||
// collecter en Vec<(Uuid, i64)>
|
||||
let channel_permissions: Vec<(Uuid, i64)> = channel_user::Entity::find()
|
||||
.filter(channel_user::Column::UserId.eq(user_id))
|
||||
.select_only()
|
||||
.column(channel_user::Column::ChannelId)
|
||||
.column(channel_user::Column::Permissions)
|
||||
.into_tuple()
|
||||
.all(&self.context.db)
|
||||
.await?;
|
||||
|
||||
Ok(PermissionContext::new(&group_masks, channel_permissions))
|
||||
}
|
||||
|
||||
pub async fn load_for_server_user(
|
||||
pub async fn get_server_stack(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
server_id: Uuid,
|
||||
) -> Result<PermissionContext, DbErr> {
|
||||
// Requête 1 : JOIN group_member → group filtre group_member.user_id = user_id ET group.server_id = server_id
|
||||
let group_masks: Vec<i64> = group::Entity::find()
|
||||
) -> Result<PermissionStack, DbErr> {
|
||||
let server = server::Entity::find_by_id(server_id)
|
||||
.one(&self.context.db)
|
||||
.await?;
|
||||
|
||||
let server = match server {
|
||||
Some(s) => s,
|
||||
None => return Ok(PermissionStack::new()),
|
||||
};
|
||||
|
||||
let mut stack = PermissionStack::new();
|
||||
stack.server = Some(server.default_permissions);
|
||||
|
||||
let server_user = 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?;
|
||||
|
||||
if let Some(su) = server_user {
|
||||
// Bypass admin/owner dans resolve() ou ici ?
|
||||
// L'utilisateur a dit "si une seule fois la permission... alors on est bon".
|
||||
// Mais admin/owner devrait tout donner.
|
||||
// On peut mettre un flag spécial ou tout mettre à 1 dans le mask.
|
||||
if su.is_admin || su.is_owner {
|
||||
// Pour simplifier, on sature le masque si admin/owner
|
||||
stack.server_user = Some(u64::MAX);
|
||||
return Ok(stack);
|
||||
}
|
||||
stack.server_user = Some(su.permissions);
|
||||
}
|
||||
|
||||
let group_permissions: Vec<u64> = group::Entity::find()
|
||||
.inner_join(group_member::Entity)
|
||||
.filter(group_member::Column::UserId.eq(user_id))
|
||||
.filter(group::Column::ServerId.eq(server_id))
|
||||
.filter(group_member::Column::UserId.eq(user_id))
|
||||
.select_only()
|
||||
.column(group::Column::Permissions)
|
||||
.into_tuple()
|
||||
.all(&self.context.db)
|
||||
.await?;
|
||||
|
||||
// Requête 2 : SELECT channel_id, permissions FROM channel_user
|
||||
// JOIN channel ON channel_user.channel_id = channel.id
|
||||
// WHERE user_id = user_id AND channel.server_id = server_id
|
||||
let channel_permissions: Vec<(Uuid, i64)> = channel_user::Entity::find()
|
||||
.inner_join(crate::models::channel::Entity)
|
||||
.filter(channel_user::Column::UserId.eq(user_id))
|
||||
.filter(crate::models::channel::Column::ServerId.eq(server_id))
|
||||
.select_only()
|
||||
.column(channel_user::Column::ChannelId)
|
||||
.column(channel_user::Column::Permissions)
|
||||
.into_tuple()
|
||||
.all(&self.context.db)
|
||||
if !group_permissions.is_empty() {
|
||||
let groups_mask = group_permissions.iter().fold(0u64, |acc, &p| acc | p);
|
||||
stack.groups = Some(groups_mask);
|
||||
}
|
||||
|
||||
Ok(stack)
|
||||
}
|
||||
|
||||
pub async fn get_channel_stack(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
channel_id: Uuid,
|
||||
) -> Result<PermissionStack, DbErr> {
|
||||
let channel = channel::Entity::find_by_id(channel_id)
|
||||
.one(&self.context.db)
|
||||
.await?;
|
||||
|
||||
Ok(PermissionContext::new(&group_masks, channel_permissions))
|
||||
let channel = match channel {
|
||||
Some(c) => c,
|
||||
None => return Ok(PermissionStack::new()),
|
||||
};
|
||||
|
||||
let mut stack = if let Some(server_id) = channel.server_id {
|
||||
self.get_server_stack(user_id, server_id).await?
|
||||
} else {
|
||||
PermissionStack::new()
|
||||
};
|
||||
|
||||
// Si on est déjà admin (u64::MAX), on s'arrête
|
||||
if stack.resolve() == u64::MAX {
|
||||
return Ok(stack);
|
||||
}
|
||||
|
||||
if let Some(dp) = channel.default_permissions {
|
||||
stack.channel_user = Some(dp);
|
||||
}
|
||||
|
||||
let chan_user = channel_user::Entity::find()
|
||||
.filter(channel_user::Column::ChannelId.eq(channel_id))
|
||||
.filter(channel_user::Column::UserId.eq(user_id))
|
||||
.one(&self.context.db)
|
||||
.await?;
|
||||
|
||||
if let Some(cu) = chan_user {
|
||||
let current = stack.channel_user.unwrap_or(0);
|
||||
stack.channel_user = Some(current | cu.permissions);
|
||||
}
|
||||
|
||||
Ok(stack)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use super::types::{ServerExplorerItem, ServerTree};
|
||||
use super::RepositoryContext;
|
||||
use crate::models::{category, channel, group, server};
|
||||
use crate::repositories::permission::PermissionRepository;
|
||||
use crate::utils::permissions::Permission;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set,
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -33,7 +37,7 @@ impl ServerRepository {
|
||||
let default_group = group::ActiveModel {
|
||||
server_id: Set(server.id),
|
||||
name: Set("Membres".to_string()),
|
||||
permissions: Set(Permission::default_permissions() as i64),
|
||||
permissions: Set(Permission::default_permissions()),
|
||||
is_default: Set(true),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -108,4 +112,17 @@ impl ServerRepository {
|
||||
|
||||
Ok(ServerTree { items })
|
||||
}
|
||||
|
||||
pub async fn has_perm(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
server_id: Uuid,
|
||||
permission: Permission,
|
||||
) -> Result<bool, DbErr> {
|
||||
let repo = PermissionRepository {
|
||||
context: self.context.clone(),
|
||||
};
|
||||
let stack = repo.get_server_stack(user_id, server_id).await?;
|
||||
Ok(stack.has(permission))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// # Système de Permissions par Bitmask
|
||||
///
|
||||
/// Un bitmask (masque de bits) est une manière compacte et efficace de stocker plusieurs
|
||||
@@ -70,67 +67,113 @@ impl Permission {
|
||||
}
|
||||
}
|
||||
|
||||
/// Contexte de permissions calculé pour un utilisateur donné.
|
||||
///
|
||||
/// Ce contexte regroupe les permissions globales (issues de la fusion de tous les groupes
|
||||
/// de l'utilisateur) et les surcharges spécifiques par canal (overrides).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PermissionContext {
|
||||
/// Union des masques de tous les groupes auxquels appartient l'utilisateur.
|
||||
pub global: u64,
|
||||
/// Masques spécifiques par canal (si présents, ils remplacent totalement le masque global
|
||||
/// pour ce canal spécifique).
|
||||
pub channel_overrides: HashMap<Uuid, u64>,
|
||||
pub struct PermissionStack {
|
||||
/// Permissions au niveau du serveur (None = non configuré)
|
||||
pub server: Option<u64>,
|
||||
|
||||
/// Permissions de TOUS les groupes de l'utilisateur (None = aucun groupe)
|
||||
pub groups: Option<u64>,
|
||||
|
||||
/// Permissions de l'utilisateur au niveau serveur (None = non défini)
|
||||
pub server_user: Option<u64>,
|
||||
|
||||
/// Permissions de TOUS les groupes au niveau serveur (None = non défini)
|
||||
pub server_groups: Option<u64>,
|
||||
|
||||
/// Permissions de l'utilisateur sur ce canal (None = non défini)
|
||||
pub channel_user: Option<u64>,
|
||||
|
||||
/// Permissions de TOUS les groupes sur ce canal (None = non défini)
|
||||
pub channel_groups: Option<u64>,
|
||||
}
|
||||
|
||||
impl PermissionContext {
|
||||
/// Crée un nouveau contexte à partir des masques bruts de la base de données.
|
||||
///
|
||||
/// - `group_masks` : Liste des permissions de chaque groupe de l'utilisateur.
|
||||
/// - `channel_permissions` : Liste de (ID Canal, Masque) pour les permissions spécifiques.
|
||||
pub fn new(group_masks: &[i64], channel_permissions: Vec<(Uuid, i64)>) -> Self {
|
||||
// On combine tous les groupes avec l'opérateur OR (|)
|
||||
let global = group_masks.iter().fold(0u64, |acc, &m| acc | m as u64);
|
||||
|
||||
let channel_overrides = channel_permissions
|
||||
.into_iter()
|
||||
.map(|(id, m)| (id, m as u64))
|
||||
.collect();
|
||||
|
||||
impl PermissionStack {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
global,
|
||||
channel_overrides,
|
||||
server: None,
|
||||
groups: None,
|
||||
server_user: None,
|
||||
server_groups: None,
|
||||
channel_user: None,
|
||||
channel_groups: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifie si une permission spécifique est accordée, optionnellement pour un canal précis.
|
||||
/// Résout les permissions finales en mode Union
|
||||
/// On accumule TOUTES les sources définies avec OR
|
||||
pub fn resolve(&self) -> u64 {
|
||||
let mut result = 0u64;
|
||||
|
||||
if let Some(perms) = self.server {
|
||||
result |= perms;
|
||||
}
|
||||
if let Some(perms) = self.groups {
|
||||
result |= perms;
|
||||
}
|
||||
if let Some(perms) = self.server_user {
|
||||
result |= perms;
|
||||
}
|
||||
if let Some(perms) = self.server_groups {
|
||||
result |= perms;
|
||||
}
|
||||
if let Some(perms) = self.channel_user {
|
||||
result |= perms;
|
||||
}
|
||||
if let Some(perms) = self.channel_groups {
|
||||
result |= perms;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Vérifie si une permission spécifique est accordée.
|
||||
///
|
||||
/// Si `channel_id` est fourni et qu'une surcharge existe pour ce canal, la surcharge
|
||||
/// est utilisée. Sinon, on utilise les permissions globales du serveur.
|
||||
///
|
||||
/// ### Exemple d'utilisation :
|
||||
/// ### Exemple :
|
||||
/// ```rust
|
||||
/// if ctx.has(Permission::SendMessage, Some(channel_id)) {
|
||||
/// // Autorisé !
|
||||
/// if stack.has(Permission::SendMessage) {
|
||||
/// println!("L'utilisateur peut envoyer des messages !");
|
||||
/// }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn has(&self, permission: Permission, channel_id: Option<Uuid>) -> bool {
|
||||
let mask = if let Some(cid) = channel_id {
|
||||
self.channel_overrides
|
||||
.get(&cid)
|
||||
.copied()
|
||||
.unwrap_or(self.global)
|
||||
} else {
|
||||
self.global
|
||||
};
|
||||
|
||||
pub fn has(&self, permission: Permission) -> bool {
|
||||
let mask = self.resolve();
|
||||
mask & (permission as u64) != 0
|
||||
}
|
||||
|
||||
/// Méthode utilitaire pour vérifier rapidement l'accès en lecture à un canal.
|
||||
pub fn can_see_channel(&self, channel_id: Uuid) -> bool {
|
||||
self.has(Permission::ReadChannel, Some(channel_id))
|
||||
/// Vérifie si TOUTES les permissions listées sont accordées (Strict).
|
||||
///
|
||||
/// Utile pour des actions nécessitant plusieurs droits combinés.
|
||||
///
|
||||
/// ### Exemple :
|
||||
/// ```rust
|
||||
/// // L'utilisateur doit pouvoir voir ET parler pour rejoindre un vocal
|
||||
/// let perms = [Permission::ReadChannel, Permission::VoiceSpeak];
|
||||
/// if stack.has_all(&perms) {
|
||||
/// join_voice_channel();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn has_all(&self, permissions: &[Permission]) -> bool {
|
||||
let required = permissions.iter().fold(0u64, |acc, &p| acc | p as u64);
|
||||
let mask = self.resolve();
|
||||
(mask & required) == required
|
||||
}
|
||||
|
||||
/// Vérifie si AU MOINS UNE des permissions listées est accordée.
|
||||
///
|
||||
/// Utile pour les rôles de modération ou les accès "ou" (OR).
|
||||
///
|
||||
/// ### Exemple :
|
||||
/// ```rust
|
||||
/// // L'utilisateur peut supprimer le message s'il est modérateur OU admin
|
||||
/// let moderator_perms = [Permission::DeleteOthersMessage, Permission::ManageChannel];
|
||||
/// if stack.has_any(&moderator_perms) {
|
||||
/// delete_message();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn has_any(&self, permissions: &[Permission]) -> bool {
|
||||
let required = permissions.iter().fold(0u64, |acc, &p| acc | p as u64);
|
||||
let mask = self.resolve();
|
||||
(mask & required) != 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,45 +182,32 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_permission_context() {
|
||||
let channel_id = Uuid::new_v4();
|
||||
let other_channel_id = Uuid::new_v4();
|
||||
fn test_permission_stack() {
|
||||
let mut stack = PermissionStack::new();
|
||||
|
||||
// On imagine un utilisateur avec 2 groupes
|
||||
let role_masks = vec![
|
||||
Permission::ReadChannel as i64 | Permission::SendMessage as i64, // Groupe 1 : lecture + envoi
|
||||
Permission::VoiceSpeak as i64, // Groupe 2 : vocal
|
||||
];
|
||||
// 1. Test des permissions par défaut du serveur
|
||||
stack.server = Some(Permission::ReadChannel as u64 | Permission::SendMessage as u64);
|
||||
assert!(stack.has(Permission::ReadChannel));
|
||||
assert!(stack.has(Permission::SendMessage));
|
||||
assert!(!stack.has(Permission::VoiceSpeak));
|
||||
|
||||
// Et une permission spécifique sur un canal (Lecture + Envoi + Gestion)
|
||||
let channel_permissions = vec![(
|
||||
channel_id,
|
||||
Permission::ReadChannel as i64
|
||||
| Permission::SendMessage as i64
|
||||
| Permission::ManageChannel as i64,
|
||||
)];
|
||||
// 2. Test de l'union avec les groupes
|
||||
stack.groups = Some(Permission::VoiceSpeak as u64);
|
||||
assert!(stack.has(Permission::ReadChannel));
|
||||
assert!(stack.has(Permission::SendMessage));
|
||||
assert!(stack.has(Permission::VoiceSpeak));
|
||||
|
||||
let ctx = PermissionContext::new(&role_masks, channel_permissions);
|
||||
// 3. Test du bypass administrateur (Saturation du masque)
|
||||
stack.server_user = Some(u64::MAX);
|
||||
assert!(stack.has(Permission::ManageServer));
|
||||
assert!(stack.has(Permission::ManageRoles));
|
||||
assert!(stack.has(Permission::KickMember));
|
||||
|
||||
// Test des permissions globales
|
||||
assert!(ctx.has(Permission::ReadChannel, None));
|
||||
assert!(ctx.has(Permission::SendMessage, None));
|
||||
assert!(ctx.has(Permission::VoiceSpeak, None));
|
||||
assert!(!ctx.has(Permission::ManageChannel, None));
|
||||
|
||||
// Test des surcharges par canal (L'override remplace le masque global)
|
||||
assert!(ctx.has(Permission::ReadChannel, Some(channel_id)));
|
||||
assert!(ctx.has(Permission::SendMessage, Some(channel_id)));
|
||||
assert!(ctx.has(Permission::ManageChannel, Some(channel_id)));
|
||||
assert!(!ctx.has(Permission::VoiceSpeak, Some(channel_id))); // Ici on perd le vocal car l'override remplace tout
|
||||
|
||||
// Test d'un autre canal (pas de surcharge, on retombe sur le global)
|
||||
assert!(ctx.has(Permission::ReadChannel, Some(other_channel_id)));
|
||||
assert!(ctx.has(Permission::VoiceSpeak, Some(other_channel_id)));
|
||||
assert!(!ctx.has(Permission::ManageChannel, Some(other_channel_id)));
|
||||
|
||||
// Test des fonctions sucre
|
||||
assert!(ctx.can_see_channel(channel_id));
|
||||
assert!(ctx.can_see_channel(other_channel_id));
|
||||
// 4. Test spécifique au canal (Sans bypass admin)
|
||||
stack.server_user = None;
|
||||
stack.channel_user = Some(Permission::ManageChannel as u64);
|
||||
assert!(stack.has(Permission::ReadChannel)); // Vient du serveur
|
||||
assert!(stack.has(Permission::ManageChannel)); // Vient du channel_user
|
||||
assert!(!stack.has(Permission::ManageRoles)); // Pas défini
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user