Init
This commit is contained in:
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -1580,6 +1580,12 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"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]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
@@ -3731,6 +3737,19 @@ dependencies = [
|
|||||||
"tungstenite",
|
"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]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
@@ -3806,12 +3825,19 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
|
"http-range-header",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ utoipa-scalar = { version = "0.3", features = ["axum"] }
|
|||||||
utoipa-swagger-ui = { version = "9.0", features = ["axum"] }
|
utoipa-swagger-ui = { version = "9.0", features = ["axum"] }
|
||||||
utoipa-axum = "0.2"
|
utoipa-axum = "0.2"
|
||||||
tower = { version = "0.5", features = ["util"] }
|
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
|
# UDP
|
||||||
socket2 = "0.6"
|
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
|
## TODO
|
||||||
|
|
||||||
Terminer le système d'authentification (login, changement de mot de passe...)
|
Terminer le système d'authentification (login, changement de mot de passe...)
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ impl MigrationTrait for Migration {
|
|||||||
.not_null()
|
.not_null()
|
||||||
.default(Expr::current_timestamp()),
|
.default(Expr::current_timestamp()),
|
||||||
)
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("default_permissions"))
|
||||||
|
.big_integer()
|
||||||
|
.not_null()
|
||||||
|
.default(0),
|
||||||
|
)
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -114,6 +120,11 @@ impl MigrationTrait for Migration {
|
|||||||
.not_null()
|
.not_null()
|
||||||
.default(Expr::current_timestamp()),
|
.default(Expr::current_timestamp()),
|
||||||
)
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Alias::new("default_permissions"))
|
||||||
|
.big_integer()
|
||||||
|
.null(),
|
||||||
|
)
|
||||||
// Indexes créés après
|
// Indexes créés après
|
||||||
.foreign_key(
|
.foreign_key(
|
||||||
ForeignKey::create()
|
ForeignKey::create()
|
||||||
@@ -299,6 +310,18 @@ impl MigrationTrait for Migration {
|
|||||||
.not_null()
|
.not_null()
|
||||||
.default(false),
|
.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
|
// Indexes créés après
|
||||||
.foreign_key(
|
.foreign_key(
|
||||||
ForeignKey::create()
|
ForeignKey::create()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub struct GroupResponse {
|
|||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub server_id: Uuid,
|
pub server_id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub permissions: i64,
|
pub permissions: u64,
|
||||||
pub is_default: bool,
|
pub is_default: bool,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ impl From<group::Model> for GroupResponse {
|
|||||||
pub struct CreateGroupRequest {
|
pub struct CreateGroupRequest {
|
||||||
pub server_id: Uuid,
|
pub server_id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub permissions: i64,
|
pub permissions: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreateGroupRequest {
|
impl CreateGroupRequest {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ pub struct Model {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTimeUtc,
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
|
pub default_permissions: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub struct Model {
|
|||||||
pub channel_id: Uuid,
|
pub channel_id: Uuid,
|
||||||
pub user_id: Uuid,
|
pub user_id: Uuid,
|
||||||
pub role: String,
|
pub role: String,
|
||||||
pub permissions: i64,
|
pub permissions: u64,
|
||||||
pub joined_at: DateTimeUtc,
|
pub joined_at: DateTimeUtc,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub struct Model {
|
|||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub server_id: Uuid,
|
pub server_id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub permissions: i64,
|
pub permissions: u64,
|
||||||
pub is_default: bool,
|
pub is_default: bool,
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTimeUtc,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub struct Model {
|
|||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTimeUtc,
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
|
pub default_permissions: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ pub struct Model {
|
|||||||
pub joined_at: DateTimeUtc,
|
pub joined_at: DateTimeUtc,
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
pub is_admin: bool,
|
pub is_admin: bool,
|
||||||
|
pub is_owner: bool,
|
||||||
|
pub permissions: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|||||||
@@ -97,23 +97,12 @@ pub async fn category_create(
|
|||||||
user: CurrentUser,
|
user: CurrentUser,
|
||||||
Json(payload): Json<CreateCategoryRequest>,
|
Json(payload): Json<CreateCategoryRequest>,
|
||||||
) -> Result<Json<CategoryResponse>, HTTPError> {
|
) -> Result<Json<CategoryResponse>, HTTPError> {
|
||||||
let is_admin = app_state
|
let has_permission = user.is_superuser
|
||||||
.repositories
|
|| app_state
|
||||||
.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
|
|
||||||
.repositories
|
.repositories
|
||||||
.permission
|
.server
|
||||||
.load_for_server_user(user.id, payload.server_id)
|
.has_perm(user.id, payload.server_id, Permission::ManageServer)
|
||||||
.await?;
|
.await?;
|
||||||
ctx.has(Permission::ManageServer, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !has_permission {
|
if !has_permission {
|
||||||
return Err(HTTPError::Forbidden);
|
return Err(HTTPError::Forbidden);
|
||||||
@@ -149,23 +138,12 @@ pub async fn category_update(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| HTTPError::NotFound)?;
|
.ok_or_else(|| HTTPError::NotFound)?;
|
||||||
|
|
||||||
let is_admin = app_state
|
let has_permission = user.is_superuser
|
||||||
.repositories
|
|| app_state
|
||||||
.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
|
|
||||||
.repositories
|
.repositories
|
||||||
.permission
|
.server
|
||||||
.load_for_server_user(user.id, category.server_id)
|
.has_perm(user.id, category.server_id, Permission::ManageServer)
|
||||||
.await?;
|
.await?;
|
||||||
ctx.has(Permission::ManageServer, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !has_permission {
|
if !has_permission {
|
||||||
return Err(HTTPError::Forbidden);
|
return Err(HTTPError::Forbidden);
|
||||||
@@ -203,23 +181,12 @@ pub async fn category_delete(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| HTTPError::NotFound)?;
|
.ok_or_else(|| HTTPError::NotFound)?;
|
||||||
|
|
||||||
let is_admin = app_state
|
let has_permission = user.is_superuser
|
||||||
.repositories
|
|| app_state
|
||||||
.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
|
|
||||||
.repositories
|
.repositories
|
||||||
.permission
|
.server
|
||||||
.load_for_server_user(user.id, category.server_id)
|
.has_perm(user.id, category.server_id, Permission::ManageServer)
|
||||||
.await?;
|
.await?;
|
||||||
ctx.has(Permission::ManageServer, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !has_permission {
|
if !has_permission {
|
||||||
return Err(HTTPError::Forbidden);
|
return Err(HTTPError::Forbidden);
|
||||||
|
|||||||
@@ -79,23 +79,12 @@ pub async fn channel_create(
|
|||||||
Json(dto): Json<CreateChannelRequest>,
|
Json(dto): Json<CreateChannelRequest>,
|
||||||
) -> Result<Json<ChannelResponse>, HTTPError> {
|
) -> Result<Json<ChannelResponse>, HTTPError> {
|
||||||
if let Some(server_id) = dto.server_id {
|
if let Some(server_id) = dto.server_id {
|
||||||
let is_admin = app_state
|
let has_permission = user.is_superuser
|
||||||
.repositories
|
|| app_state
|
||||||
.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
|
|
||||||
.repositories
|
.repositories
|
||||||
.permission
|
.server
|
||||||
.load_for_server_user(user.id, server_id)
|
.has_perm(user.id, server_id, Permission::ManageServer)
|
||||||
.await?;
|
.await?;
|
||||||
ctx.has(Permission::ManageServer, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !has_permission {
|
if !has_permission {
|
||||||
return Err(HTTPError::Forbidden);
|
return Err(HTTPError::Forbidden);
|
||||||
@@ -133,23 +122,17 @@ pub async fn channel_update(
|
|||||||
.ok_or(HTTPError::NotFound)?;
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
if let Some(server_id) = channel.server_id {
|
if let Some(server_id) = channel.server_id {
|
||||||
let is_admin = app_state
|
let has_permission = user.is_superuser
|
||||||
.repositories
|
|| app_state
|
||||||
.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
|
|
||||||
.repositories
|
.repositories
|
||||||
.permission
|
.channel
|
||||||
.load_for_server_user(user.id, server_id)
|
.has_perm(user.id, id, Permission::ManageChannel)
|
||||||
|
.await?
|
||||||
|
|| app_state
|
||||||
|
.repositories
|
||||||
|
.server
|
||||||
|
.has_perm(user.id, server_id, Permission::ManageServer)
|
||||||
.await?;
|
.await?;
|
||||||
ctx.has(Permission::ManageChannel, Some(id)) || ctx.has(Permission::ManageServer, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !has_permission {
|
if !has_permission {
|
||||||
return Err(HTTPError::Forbidden);
|
return Err(HTTPError::Forbidden);
|
||||||
@@ -189,23 +172,17 @@ pub async fn channel_delete(
|
|||||||
.ok_or(HTTPError::NotFound)?;
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
if let Some(server_id) = channel.server_id {
|
if let Some(server_id) = channel.server_id {
|
||||||
let is_admin = app_state
|
let has_permission = user.is_superuser
|
||||||
.repositories
|
|| app_state
|
||||||
.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
|
|
||||||
.repositories
|
.repositories
|
||||||
.permission
|
.channel
|
||||||
.load_for_server_user(user.id, server_id)
|
.has_perm(user.id, id, Permission::ManageChannel)
|
||||||
|
.await?
|
||||||
|
|| app_state
|
||||||
|
.repositories
|
||||||
|
.server
|
||||||
|
.has_perm(user.id, server_id, Permission::ManageServer)
|
||||||
.await?;
|
.await?;
|
||||||
ctx.has(Permission::ManageChannel, Some(id)) || ctx.has(Permission::ManageServer, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !has_permission {
|
if !has_permission {
|
||||||
return Err(HTTPError::Forbidden);
|
return Err(HTTPError::Forbidden);
|
||||||
|
|||||||
@@ -87,23 +87,12 @@ pub async fn group_create(
|
|||||||
user: CurrentUser,
|
user: CurrentUser,
|
||||||
Json(payload): Json<CreateGroupRequest>,
|
Json(payload): Json<CreateGroupRequest>,
|
||||||
) -> Result<Json<GroupResponse>, HTTPError> {
|
) -> Result<Json<GroupResponse>, HTTPError> {
|
||||||
let is_admin = state
|
let has_permission = user.is_superuser
|
||||||
.repositories
|
|| state
|
||||||
.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
|
|
||||||
.repositories
|
.repositories
|
||||||
.permission
|
.server
|
||||||
.load_for_server_user(user.id, payload.server_id)
|
.has_perm(user.id, payload.server_id, Permission::ManageRoles)
|
||||||
.await?;
|
.await?;
|
||||||
ctx.has(Permission::ManageRoles, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !has_permission {
|
if !has_permission {
|
||||||
return Err(HTTPError::Forbidden);
|
return Err(HTTPError::Forbidden);
|
||||||
@@ -141,23 +130,12 @@ pub async fn group_update(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(HTTPError::NotFound)?;
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
let is_admin = state
|
let has_permission = user.is_superuser
|
||||||
.repositories
|
|| state
|
||||||
.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
|
|
||||||
.repositories
|
.repositories
|
||||||
.permission
|
.server
|
||||||
.load_for_server_user(user.id, group.server_id)
|
.has_perm(user.id, group.server_id, Permission::ManageRoles)
|
||||||
.await?;
|
.await?;
|
||||||
ctx.has(Permission::ManageRoles, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !has_permission {
|
if !has_permission {
|
||||||
return Err(HTTPError::Forbidden);
|
return Err(HTTPError::Forbidden);
|
||||||
@@ -197,23 +175,12 @@ pub async fn group_delete(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(HTTPError::NotFound)?;
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
let is_admin = state
|
let has_permission = user.is_superuser
|
||||||
.repositories
|
|| state
|
||||||
.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
|
|
||||||
.repositories
|
.repositories
|
||||||
.permission
|
.server
|
||||||
.load_for_server_user(user.id, group.server_id)
|
.has_perm(user.id, group.server_id, Permission::ManageRoles)
|
||||||
.await?;
|
.await?;
|
||||||
ctx.has(Permission::ManageRoles, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
if !has_permission {
|
if !has_permission {
|
||||||
return Err(HTTPError::Forbidden);
|
return Err(HTTPError::Forbidden);
|
||||||
|
|||||||
@@ -88,22 +88,15 @@ pub async fn message_create(
|
|||||||
.ok_or(HTTPError::NotFound)?;
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
if let Some(server_id) = channel.server_id {
|
if let Some(server_id) = channel.server_id {
|
||||||
let is_admin = app_state
|
let has_permission = user.is_superuser
|
||||||
.repositories
|
|| app_state
|
||||||
.permission
|
|
||||||
.is_server_admin(user.id, server_id)
|
|
||||||
.await?;
|
|
||||||
let is_superuser = user.is_superuser;
|
|
||||||
|
|
||||||
if !is_admin && !is_superuser {
|
|
||||||
let ctx = app_state
|
|
||||||
.repositories
|
.repositories
|
||||||
.permission
|
.channel
|
||||||
.load_for_server_user(user.id, server_id)
|
.has_perm(user.id, channel.id, Permission::SendMessage)
|
||||||
.await?;
|
.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?
|
.await?
|
||||||
.ok_or(HTTPError::NotFound)?;
|
.ok_or(HTTPError::NotFound)?;
|
||||||
|
|
||||||
if let Some(server_id) = channel.server_id {
|
let has_permission = user.is_superuser
|
||||||
let is_admin = app_state
|
|| app_state
|
||||||
.repositories
|
.repositories
|
||||||
.permission
|
.channel
|
||||||
.is_server_admin(user.id, server_id)
|
.has_perm(user.id, channel.id, Permission::DeleteOthersMessage)
|
||||||
.await?;
|
.await?;
|
||||||
let is_superuser = user.is_superuser;
|
|
||||||
|
|
||||||
if !is_admin && !is_superuser {
|
if !has_permission {
|
||||||
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 {
|
|
||||||
return Err(HTTPError::Forbidden);
|
return Err(HTTPError::Forbidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::interfaces::http::dto::{
|
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,
|
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::api::{auth, category, channel, message, server, user};
|
||||||
|
use crate::network::http::web::ws_handler;
|
||||||
use utoipa::OpenApi;
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
@@ -36,6 +37,7 @@ use utoipa::OpenApi;
|
|||||||
user::get_me,
|
user::get_me,
|
||||||
user::user_list,
|
user::user_list,
|
||||||
user::user_detail,
|
user::user_detail,
|
||||||
|
ws_handler::ws_doc,
|
||||||
),
|
),
|
||||||
components(
|
components(
|
||||||
schemas(
|
schemas(
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ use crate::interfaces::http::dto::server::{
|
|||||||
use crate::models::server;
|
use crate::models::server;
|
||||||
use crate::network::http::context::CurrentUser;
|
use crate::network::http::context::CurrentUser;
|
||||||
use crate::network::http::{AppRouter, HTTPError};
|
use crate::network::http::{AppRouter, HTTPError};
|
||||||
|
use crate::utils::permissions::Permission;
|
||||||
|
use axum::Json;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::Json;
|
|
||||||
use sea_orm::IntoActiveModel;
|
use sea_orm::IntoActiveModel;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -107,14 +108,14 @@ pub async fn server_update(
|
|||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
Json(serializer): Json<CreateServerRequest>,
|
Json(serializer): Json<CreateServerRequest>,
|
||||||
) -> Result<Json<ServerResponse>, HTTPError> {
|
) -> Result<Json<ServerResponse>, HTTPError> {
|
||||||
let is_admin = state
|
let has_permission = user.is_superuser
|
||||||
.repositories
|
|| state
|
||||||
.permission
|
.repositories
|
||||||
.is_server_admin(user.id, id)
|
.server
|
||||||
.await?;
|
.has_perm(user.id, id, Permission::ManageServer)
|
||||||
let is_superuser = user.is_superuser;
|
.await?;
|
||||||
|
|
||||||
if !is_admin && !is_superuser {
|
if !has_permission {
|
||||||
return Err(HTTPError::Forbidden);
|
return Err(HTTPError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,14 +153,14 @@ pub async fn server_delete(
|
|||||||
user: CurrentUser,
|
user: CurrentUser,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
) -> Result<StatusCode, HTTPError> {
|
) -> Result<StatusCode, HTTPError> {
|
||||||
let is_admin = state
|
let has_permission = user.is_superuser
|
||||||
.repositories
|
|| state
|
||||||
.permission
|
.repositories
|
||||||
.is_server_admin(user.id, id)
|
.server
|
||||||
.await?;
|
.has_perm(user.id, id, Permission::ManageServer)
|
||||||
let is_superuser = user.is_superuser;
|
.await?;
|
||||||
|
|
||||||
if !is_admin && !is_superuser {
|
if !has_permission {
|
||||||
return Err(HTTPError::Forbidden);
|
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::app::AppState;
|
||||||
use crate::hub::Client;
|
use crate::hub::Client;
|
||||||
use crate::network::http::AppRouter;
|
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 {
|
pub fn setup_route() -> AppRouter {
|
||||||
AppRouter::new()
|
AppRouter::new().route("/ws/", get(ws_handler))
|
||||||
.route("/ws/", get(ws_handler))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ws_handler(
|
/// WebSocket documentation placeholder.
|
||||||
ws: WebSocketUpgrade,
|
///
|
||||||
State(app_state): State<AppState>
|
/// This is not a real HTTP endpoint, but it documents the WebSocket behavior.
|
||||||
) -> impl IntoResponse {
|
///
|
||||||
|
/// **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 ---
|
// todo --- 1. VÉRIFICATION AVANT UPGRADE ---
|
||||||
// C'est ici qu'on vérifierait le JWT par exemple.
|
// C'est ici qu'on vérifierait le JWT par exemple.
|
||||||
// Si ça échoue, on peut retourner une erreur HTTP direct.
|
// 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;
|
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 {
|
let mut send_task = tokio::spawn(async move {
|
||||||
while let Some(msg) = rx.recv().await {
|
while let Some(msg) = rx.recv().await {
|
||||||
if ws_sender.send(msg).await.is_err() {
|
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;
|
client.on_disconnect().await;
|
||||||
app_state.clients.remove_client(connection_id);
|
app_state.clients.remove_client(connection_id);
|
||||||
println!("Session terminée pour le 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::models::channel;
|
||||||
|
use crate::repositories::permission::PermissionRepository;
|
||||||
use crate::repositories::RepositoryContext;
|
use crate::repositories::RepositoryContext;
|
||||||
|
use crate::utils::permissions::Permission;
|
||||||
|
use sea_orm::{ActiveModelTrait, DbErr, EntityTrait};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ChannelRepository {
|
pub struct ChannelRepository {
|
||||||
pub context: Arc<RepositoryContext>
|
pub context: Arc<RepositoryContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChannelRepository {
|
impl ChannelRepository {
|
||||||
@@ -26,8 +28,23 @@ impl ChannelRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
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);
|
self.context.events.emit("channel_deleted", id);
|
||||||
Ok(())
|
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::repositories::RepositoryContext;
|
||||||
use crate::utils::permissions::PermissionContext;
|
use crate::utils::permissions::PermissionStack;
|
||||||
use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect};
|
use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -11,72 +11,100 @@ pub struct PermissionRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PermissionRepository {
|
impl PermissionRepository {
|
||||||
pub async fn is_server_admin(&self, user_id: Uuid, server_id: Uuid) -> Result<bool, DbErr> {
|
pub async fn get_server_stack(
|
||||||
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(
|
|
||||||
&self,
|
&self,
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
server_id: Uuid,
|
server_id: Uuid,
|
||||||
) -> Result<PermissionContext, DbErr> {
|
) -> Result<PermissionStack, DbErr> {
|
||||||
// Requête 1 : JOIN group_member → group filtre group_member.user_id = user_id ET group.server_id = server_id
|
let server = server::Entity::find_by_id(server_id)
|
||||||
let group_masks: Vec<i64> = group::Entity::find()
|
.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)
|
.inner_join(group_member::Entity)
|
||||||
.filter(group_member::Column::UserId.eq(user_id))
|
|
||||||
.filter(group::Column::ServerId.eq(server_id))
|
.filter(group::Column::ServerId.eq(server_id))
|
||||||
|
.filter(group_member::Column::UserId.eq(user_id))
|
||||||
.select_only()
|
.select_only()
|
||||||
.column(group::Column::Permissions)
|
.column(group::Column::Permissions)
|
||||||
.into_tuple()
|
.into_tuple()
|
||||||
.all(&self.context.db)
|
.all(&self.context.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Requête 2 : SELECT channel_id, permissions FROM channel_user
|
if !group_permissions.is_empty() {
|
||||||
// JOIN channel ON channel_user.channel_id = channel.id
|
let groups_mask = group_permissions.iter().fold(0u64, |acc, &p| acc | p);
|
||||||
// WHERE user_id = user_id AND channel.server_id = server_id
|
stack.groups = Some(groups_mask);
|
||||||
let channel_permissions: Vec<(Uuid, i64)> = channel_user::Entity::find()
|
}
|
||||||
.inner_join(crate::models::channel::Entity)
|
|
||||||
.filter(channel_user::Column::UserId.eq(user_id))
|
Ok(stack)
|
||||||
.filter(crate::models::channel::Column::ServerId.eq(server_id))
|
}
|
||||||
.select_only()
|
|
||||||
.column(channel_user::Column::ChannelId)
|
pub async fn get_channel_stack(
|
||||||
.column(channel_user::Column::Permissions)
|
&self,
|
||||||
.into_tuple()
|
user_id: Uuid,
|
||||||
.all(&self.context.db)
|
channel_id: Uuid,
|
||||||
|
) -> Result<PermissionStack, DbErr> {
|
||||||
|
let channel = channel::Entity::find_by_id(channel_id)
|
||||||
|
.one(&self.context.db)
|
||||||
.await?;
|
.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::types::{ServerExplorerItem, ServerTree};
|
||||||
use super::RepositoryContext;
|
use super::RepositoryContext;
|
||||||
use crate::models::{category, channel, group, server};
|
use crate::models::{category, channel, group, server};
|
||||||
|
use crate::repositories::permission::PermissionRepository;
|
||||||
use crate::utils::permissions::Permission;
|
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 std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -33,7 +37,7 @@ impl ServerRepository {
|
|||||||
let default_group = group::ActiveModel {
|
let default_group = group::ActiveModel {
|
||||||
server_id: Set(server.id),
|
server_id: Set(server.id),
|
||||||
name: Set("Membres".to_string()),
|
name: Set("Membres".to_string()),
|
||||||
permissions: Set(Permission::default_permissions() as i64),
|
permissions: Set(Permission::default_permissions()),
|
||||||
is_default: Set(true),
|
is_default: Set(true),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -108,4 +112,17 @@ impl ServerRepository {
|
|||||||
|
|
||||||
Ok(ServerTree { items })
|
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
|
/// # Système de Permissions par Bitmask
|
||||||
///
|
///
|
||||||
/// Un bitmask (masque de bits) est une manière compacte et efficace de stocker plusieurs
|
/// 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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PermissionContext {
|
pub struct PermissionStack {
|
||||||
/// Union des masques de tous les groupes auxquels appartient l'utilisateur.
|
/// Permissions au niveau du serveur (None = non configuré)
|
||||||
pub global: u64,
|
pub server: Option<u64>,
|
||||||
/// Masques spécifiques par canal (si présents, ils remplacent totalement le masque global
|
|
||||||
/// pour ce canal spécifique).
|
/// Permissions de TOUS les groupes de l'utilisateur (None = aucun groupe)
|
||||||
pub channel_overrides: HashMap<Uuid, u64>,
|
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 {
|
impl PermissionStack {
|
||||||
/// Crée un nouveau contexte à partir des masques bruts de la base de données.
|
pub fn new() -> Self {
|
||||||
///
|
|
||||||
/// - `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();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
global,
|
server: None,
|
||||||
channel_overrides,
|
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
|
/// ### Exemple :
|
||||||
/// est utilisée. Sinon, on utilise les permissions globales du serveur.
|
|
||||||
///
|
|
||||||
/// ### Exemple d'utilisation :
|
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// if ctx.has(Permission::SendMessage, Some(channel_id)) {
|
/// if stack.has(Permission::SendMessage) {
|
||||||
/// // Autorisé !
|
/// println!("L'utilisateur peut envoyer des messages !");
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[inline]
|
pub fn has(&self, permission: Permission) -> bool {
|
||||||
pub fn has(&self, permission: Permission, channel_id: Option<Uuid>) -> bool {
|
let mask = self.resolve();
|
||||||
let mask = if let Some(cid) = channel_id {
|
|
||||||
self.channel_overrides
|
|
||||||
.get(&cid)
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(self.global)
|
|
||||||
} else {
|
|
||||||
self.global
|
|
||||||
};
|
|
||||||
|
|
||||||
mask & (permission as u64) != 0
|
mask & (permission as u64) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Méthode utilitaire pour vérifier rapidement l'accès en lecture à un canal.
|
/// Vérifie si TOUTES les permissions listées sont accordées (Strict).
|
||||||
pub fn can_see_channel(&self, channel_id: Uuid) -> bool {
|
///
|
||||||
self.has(Permission::ReadChannel, Some(channel_id))
|
/// 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::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_permission_context() {
|
fn test_permission_stack() {
|
||||||
let channel_id = Uuid::new_v4();
|
let mut stack = PermissionStack::new();
|
||||||
let other_channel_id = Uuid::new_v4();
|
|
||||||
|
|
||||||
// On imagine un utilisateur avec 2 groupes
|
// 1. Test des permissions par défaut du serveur
|
||||||
let role_masks = vec![
|
stack.server = Some(Permission::ReadChannel as u64 | Permission::SendMessage as u64);
|
||||||
Permission::ReadChannel as i64 | Permission::SendMessage as i64, // Groupe 1 : lecture + envoi
|
assert!(stack.has(Permission::ReadChannel));
|
||||||
Permission::VoiceSpeak as i64, // Groupe 2 : vocal
|
assert!(stack.has(Permission::SendMessage));
|
||||||
];
|
assert!(!stack.has(Permission::VoiceSpeak));
|
||||||
|
|
||||||
// Et une permission spécifique sur un canal (Lecture + Envoi + Gestion)
|
// 2. Test de l'union avec les groupes
|
||||||
let channel_permissions = vec![(
|
stack.groups = Some(Permission::VoiceSpeak as u64);
|
||||||
channel_id,
|
assert!(stack.has(Permission::ReadChannel));
|
||||||
Permission::ReadChannel as i64
|
assert!(stack.has(Permission::SendMessage));
|
||||||
| Permission::SendMessage as i64
|
assert!(stack.has(Permission::VoiceSpeak));
|
||||||
| Permission::ManageChannel as i64,
|
|
||||||
)];
|
|
||||||
|
|
||||||
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
|
// 4. Test spécifique au canal (Sans bypass admin)
|
||||||
assert!(ctx.has(Permission::ReadChannel, None));
|
stack.server_user = None;
|
||||||
assert!(ctx.has(Permission::SendMessage, None));
|
stack.channel_user = Some(Permission::ManageChannel as u64);
|
||||||
assert!(ctx.has(Permission::VoiceSpeak, None));
|
assert!(stack.has(Permission::ReadChannel)); // Vient du serveur
|
||||||
assert!(!ctx.has(Permission::ManageChannel, None));
|
assert!(stack.has(Permission::ManageChannel)); // Vient du channel_user
|
||||||
|
assert!(!stack.has(Permission::ManageRoles)); // Pas défini
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user