This commit is contained in:
2026-08-16 13:00:29 +02:00
parent e6d6968e52
commit 7fc8edd1d0
71 changed files with 2359 additions and 448 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
use axum::{routing::get, Router};
use axum::{Router, routing::get};
use super::handlers;
+3 -3
View File
@@ -1,13 +1,13 @@
use crate::domain::dto::auth::{LoginRequest, LoginResponse, MeResponse};
use crate::auth::token::create_jwt;
use crate::core::AppState;
use crate::domain::dto::auth::{LoginRequest, LoginResponse, MeResponse};
use crate::http::context::CurrentUser;
use crate::http::error::HTTPError;
use crate::routes::user::mapper::user_model_to_user_response;
use axum::extract::State;
use axum::Json;
use axum_extra::extract::cookie::{Cookie, SameSite};
use axum::extract::State;
use axum_extra::extract::CookieJar;
use axum_extra::extract::cookie::{Cookie, SameSite};
use sea_orm::ActiveModelBehavior;
#[utoipa::path(
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::http::OxRouter;
use crate::routes::auth::handlers;
use axum::routing::{get, post};
use axum::Router;
use axum::routing::{get, post};
pub fn router() -> OxRouter {
Router::new()
+18 -6
View File
@@ -1,14 +1,14 @@
use crate::core::state::AppState;
use crate::http::context::Superuser;
use crate::http::error::HTTPError;
use crate::domain::dto::category::{
CategoryQueryParams, CategoryResponse, CreateCategoryRequest, UpdateCategoryRequest,
};
use crate::http::context::Superuser;
use crate::http::error::HTTPError;
use crate::routes::category::mapper;
use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
Json,
};
use uuid::Uuid;
@@ -29,7 +29,11 @@ pub async fn get_all(
State(state): State<AppState>,
Query(filters): Query<CategoryQueryParams>,
) -> Result<Json<Vec<CategoryResponse>>, HTTPError> {
let categories = state.repositories.category.filter(filters.server_id).await?;
let categories = state
.repositories
.category
.filter(filters.server_id)
.await?;
Ok(Json(
categories
.into_iter()
@@ -94,7 +98,11 @@ pub async fn create(
.await?
.ok_or(HTTPError::BadRequest("Server not found".to_string()))?;
let category = state.services.category.create_category(payload.server_id, payload.name).await?;
let category = state
.services
.category
.create_category(payload.server_id, payload.name)
.await?;
Ok((
StatusCode::CREATED,
Json(mapper::category_model_to_category_response(category)),
@@ -133,7 +141,11 @@ pub async fn update(
.await?
.ok_or(HTTPError::NotFound)?;
let category = state.services.category.update_category(id, payload.name).await?;
let category = state
.services
.category
.update_category(id, payload.name)
.await?;
Ok(Json(mapper::category_model_to_category_response(category)))
}
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::core::state::AppState;
use axum::{routing::get, Router};
use axum::{Router, routing::get};
use super::handlers;
+31 -11
View File
@@ -1,12 +1,11 @@
use crate::core::state::AppState;
use crate::domain::dto::channel::{
ChannelPermissionsResponse, ChannelQueryParams, ChannelResponse, ChannelRolePermissionResponse,
ChannelUserPermissionResponse, CreateChannelRequest, ReadStateResponse,
SetChannelPermissionRequest, SetReadStateRequest, UpdateChannelRequest,
};
use crate::http::context::{CurrentUser, Superuser};
use crate::http::error::HTTPError;
use crate::domain::dto::channel::{
ChannelQueryParams, ChannelResponse, ChannelPermissionsResponse, ChannelRolePermissionResponse,
ChannelUserPermissionResponse, CreateChannelRequest, ReadStateResponse,
SetChannelPermissionRequest, SetReadStateRequest,
UpdateChannelRequest,
};
use crate::routes::channel::mapper;
use axum::{
Json,
@@ -55,8 +54,17 @@ pub async fn get_read_state(
State(state): State<AppState>,
Path(channel_id): Path<Uuid>,
) -> Result<Json<ReadStateResponse>, HTTPError> {
state.repositories.channel.get_by_id(channel_id).await?.ok_or(HTTPError::NotFound)?;
let read_state = state.repositories.read_state.get(channel_id, user.id).await?;
state
.repositories
.channel
.get_by_id(channel_id)
.await?
.ok_or(HTTPError::NotFound)?;
let read_state = state
.repositories
.read_state
.get(channel_id, user.id)
.await?;
let unread_count = state
.repositories
.read_state
@@ -68,7 +76,9 @@ pub async fn get_read_state(
Ok(Json(ReadStateResponse {
channel_id,
last_read_message_id: read_state.as_ref().and_then(|value| value.last_read_message_id),
last_read_message_id: read_state
.as_ref()
.and_then(|value| value.last_read_message_id),
updated_at: read_state.map(|value| value.updated_at),
unread_count,
}))
@@ -89,7 +99,12 @@ pub async fn set_read_state(
Path(channel_id): Path<Uuid>,
Json(payload): Json<SetReadStateRequest>,
) -> Result<Json<ReadStateResponse>, HTTPError> {
state.repositories.channel.get_by_id(channel_id).await?.ok_or(HTTPError::NotFound)?;
state
.repositories
.channel
.get_by_id(channel_id)
.await?
.ok_or(HTTPError::NotFound)?;
if let Some(message_id) = payload.last_read_message_id {
let message = state
@@ -167,7 +182,12 @@ pub async fn list_permissions(
State(state): State<AppState>,
Path(channel_id): Path<Uuid>,
) -> Result<Json<ChannelPermissionsResponse>, HTTPError> {
state.repositories.channel.get_by_id(channel_id).await?.ok_or(HTTPError::NotFound)?;
state
.repositories
.channel
.get_by_id(channel_id)
.await?
.ok_or(HTTPError::NotFound)?;
let (users, roles) = tokio::try_join!(
state.repositories.channel.list_user_permissions(channel_id),
state.repositories.channel.list_role_permissions(channel_id),
+8 -2
View File
@@ -77,8 +77,14 @@ pub fn channel_permissions_to_response(
roles: Vec<channel_role_permission::Model>,
) -> ChannelPermissionsResponse {
ChannelPermissionsResponse {
users: users.into_iter().map(channel_user_permission_to_response).collect(),
roles: roles.into_iter().map(channel_role_permission_to_response).collect(),
users: users
.into_iter()
.map(channel_user_permission_to_response)
.collect(),
roles: roles
.into_iter()
.map(channel_role_permission_to_response)
.collect(),
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
use super::handlers;
use crate::core::state::AppState;
use axum::{routing::get, Router};
use axum::{Router, routing::get};
pub fn router() -> Router<AppState> {
Router::new()
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::core::AppState;
use crate::domain::dto::core::JoinRequest;
use crate::http::error::HTTPError;
use crate::http::validation::ValidatedJson;
use crate::domain::dto::core::JoinRequest;
use crate::routes::core::mapper::join_request_to_user_am;
use axum::extract::State;
use axum::http::StatusCode;
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::auth::password::hash_password;
use crate::models::user;
use crate::domain::dto::core::JoinRequest;
use crate::models::user;
use anyhow::Result as AnyResult;
use sea_orm::Set;
use uuid::Uuid;
+1 -1
View File
@@ -1,6 +1,6 @@
use super::handlers;
use crate::http::OxRouter;
use axum::{routing::post, Router};
use axum::{Router, routing::post};
pub fn router() -> OxRouter {
Router::new().route("/join", post(handlers::join))
+274
View File
@@ -0,0 +1,274 @@
use crate::{
core::state::AppState,
domain::dto::emoji::{EmojiQueryParams, UpdateEmojiRequest},
http::error::HTTPError,
models::emoji,
routes::emoji::mapper,
services::emoji::EmojiService,
};
use axum::{
Json,
body::Body,
extract::{Multipart, Path, Query, State},
http::{StatusCode, header},
response::Response,
};
use sea_orm::{EntityTrait, Set};
use std::path::PathBuf;
use uuid::Uuid;
fn normalize_type(value: &str) -> Result<String, HTTPError> {
if value == "unicode" || value == "custom" {
Ok(value.to_string())
} else {
Err(HTTPError::BadRequest(
"emoji_type must be unicode or custom".into(),
))
}
}
#[utoipa::path(get, path = "/emojis", params(EmojiQueryParams), responses((status = 200, body = [crate::domain::dto::emoji::EmojiResponse])), tag = "Emojis")]
pub async fn get_all(
State(state): State<AppState>,
Query(query): Query<EmojiQueryParams>,
) -> Result<Json<Vec<crate::domain::dto::emoji::EmojiResponse>>, HTTPError> {
let mut result = Vec::new();
for emoji in state.repositories.emoji.list(query.server_id).await? {
let aliases = state.repositories.emoji.aliases(emoji.id).await?;
result.push(mapper::response(emoji, mapper::aliases(aliases)));
}
Ok(Json(result))
}
#[utoipa::path(get, path = "/emojis/{id}", params(("id" = Uuid, Path)), responses((status = 200, body = crate::domain::dto::emoji::EmojiResponse)), tag = "Emojis")]
pub async fn get_by_id(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Json<crate::domain::dto::emoji::EmojiResponse>, HTTPError> {
let emoji = state
.repositories
.emoji
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
let aliases = state.repositories.emoji.aliases(id).await?;
Ok(Json(mapper::response(emoji, mapper::aliases(aliases))))
}
#[utoipa::path(post, path = "/emojis", responses((status = 201, body = crate::domain::dto::emoji::EmojiResponse)), tag = "Emojis")]
pub async fn create(
State(state): State<AppState>,
mut multipart: Multipart,
) -> Result<(StatusCode, Json<crate::domain::dto::emoji::EmojiResponse>), HTTPError> {
let mut server_id = None;
let mut emoji_type = None;
let mut unicode_sequence = None;
let mut aliases = Vec::new();
let mut file = None;
let mut mime = None;
let mut animated = false;
while let Some(field) = multipart
.next_field()
.await
.map_err(|e| HTTPError::BadRequest(e.to_string()))?
{
let name = field.name().unwrap_or_default().to_string();
if name == "file" {
mime = field.content_type().map(str::to_string);
file = Some(
field
.bytes()
.await
.map_err(|e| HTTPError::BadRequest(e.to_string()))?,
);
continue;
}
let value = field
.text()
.await
.map_err(|e| HTTPError::BadRequest(e.to_string()))?;
match name.as_str() {
"server_id" => {
server_id = Some(
value
.parse()
.map_err(|_| HTTPError::BadRequest("Invalid server_id".into()))?,
)
}
"emoji_type" => emoji_type = Some(value),
"unicode_sequence" => unicode_sequence = Some(value),
"alias" => aliases.push(value),
"aliases" => aliases.extend(value.split(',').map(str::to_string)),
"is_animated" => animated = value.parse().unwrap_or(false),
_ => {}
}
}
let emoji_type = normalize_type(emoji_type.as_deref().unwrap_or("custom"))?;
if emoji_type == "unicode" && unicode_sequence.is_none() {
return Err(HTTPError::BadRequest("unicode_sequence is required".into()));
}
if emoji_type == "custom" && file.is_none() {
return Err(HTTPError::BadRequest(
"file is required for custom emojis".into(),
));
}
if let Some(server_id) = server_id {
state
.repositories
.server
.get_by_id(server_id)
.await?
.ok_or(HTTPError::BadRequest("Server not found".into()))?;
}
let id = Uuid::new_v4();
let mut path = None;
let mut sha = None;
let mut size = None;
if let Some(bytes) = file {
size = Some(bytes.len() as i64);
let detected = detect_mime(&bytes)
.ok_or_else(|| HTTPError::BadRequest("Unsupported or invalid image format".into()))?;
mime = Some(detected);
let (p, h) =
EmojiService::save_asset(std::path::Path::new(&state.config.media.root), id, &bytes)
.await?;
path = Some(p);
sha = Some(h);
}
let model = emoji::ActiveModel {
id: Set(id),
server_id: Set(server_id),
emoji_type: Set(emoji_type),
unicode_sequence: Set(unicode_sequence),
file_path: Set(path),
mime_type: Set(mime),
file_size: Set(size),
is_animated: Set(animated),
sha256: Set(sha),
..Default::default()
};
let created = state.services.emoji.create(model, aliases).await?;
let aliases = state.repositories.emoji.aliases(created.id).await?;
Ok((
StatusCode::CREATED,
Json(mapper::response(created, mapper::aliases(aliases))),
))
}
#[utoipa::path(put, path = "/emojis/{id}", request_body = UpdateEmojiRequest, params(("id" = Uuid, Path)), responses((status = 200, body = crate::domain::dto::emoji::EmojiResponse)), tag = "Emojis")]
pub async fn update(
State(state): State<AppState>,
Path(id): Path<Uuid>,
Json(payload): Json<UpdateEmojiRequest>,
) -> Result<Json<crate::domain::dto::emoji::EmojiResponse>, HTTPError> {
let existing = state
.repositories
.emoji
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
let target_server_id = payload.server_id.or(existing.server_id);
let aliases_to_validate = match &payload.aliases {
Some(aliases) => aliases.clone(),
None => state
.repositories
.emoji
.aliases(id)
.await?
.into_iter()
.map(|a| a.alias)
.collect(),
};
state
.services
.emoji
.aliases_available(&aliases_to_validate, target_server_id, Some(id))
.await?;
let mut active: emoji::ActiveModel = existing.into();
if let Some(server_id) = payload.server_id {
state
.repositories
.server
.get_by_id(server_id)
.await?
.ok_or(HTTPError::BadRequest("Server not found".into()))?;
active.server_id = Set(Some(server_id));
}
if payload.unicode_sequence.is_some() {
active.unicode_sequence = Set(payload.unicode_sequence);
}
let updated = state.repositories.emoji.update(active).await?;
if let Some(ref aliases) = payload.aliases {
state
.services
.emoji
.aliases_available(aliases, updated.server_id, Some(id))
.await?;
state
.services
.emoji
.replace_aliases(id, aliases.clone())
.await?;
}
let aliases = state.repositories.emoji.aliases(id).await?;
Ok(Json(mapper::response(updated, mapper::aliases(aliases))))
}
fn detect_mime(bytes: &[u8]) -> Option<String> {
if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
Some("image/png".into())
} else if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
Some("image/gif".into())
} else if bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
Some("image/webp".into())
} else {
None
}
}
#[utoipa::path(delete, path = "/emojis/{id}", params(("id" = Uuid, Path)), responses((status = 204)), tag = "Emojis")]
pub async fn delete(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, HTTPError> {
let model = state
.repositories
.emoji
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
let deleted = state.repositories.emoji.delete(id).await?;
if deleted {
EmojiService::remove_asset(
std::path::Path::new(&state.config.media.root),
model.file_path.as_deref(),
)
.await;
Ok(StatusCode::NO_CONTENT)
} else {
Err(HTTPError::NotFound)
}
}
pub async fn asset(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Response, HTTPError> {
let model = state
.repositories
.emoji
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
let path = model.file_path.ok_or(HTTPError::NotFound)?;
let bytes = tokio::fs::read(PathBuf::from(&state.config.media.root).join(path))
.await
.map_err(|_| HTTPError::NotFound)?;
let mut response = Response::new(Body::from(bytes));
if let Some(mime) = model.mime_type {
if let Ok(value) = mime.parse() {
response.headers_mut().insert(header::CONTENT_TYPE, value);
}
}
Ok(response)
}
+25
View File
@@ -0,0 +1,25 @@
use crate::{domain::dto::emoji::EmojiResponse, models::emoji};
pub fn response(model: emoji::Model, aliases: Vec<String>) -> EmojiResponse {
EmojiResponse {
id: model.id,
server_id: model.server_id,
emoji_type: model.emoji_type,
unicode_sequence: model.unicode_sequence,
aliases,
asset_url: model
.file_path
.as_ref()
.map(|_| format!("/api/emojis/{}/asset", model.id)),
mime_type: model.mime_type,
file_size: model.file_size,
is_animated: model.is_animated,
sha256: model.sha256,
created_at: model.created_at,
updated_at: model.updated_at,
}
}
pub fn aliases(models: Vec<crate::models::emoji_alias::Model>) -> Vec<String> {
models.into_iter().map(|a| a.alias).collect()
}
+3
View File
@@ -0,0 +1,3 @@
pub mod handlers;
pub mod mapper;
pub mod routes;
+15
View File
@@ -0,0 +1,15 @@
use super::handlers;
use crate::core::state::AppState;
use axum::{Router, routing::get};
pub fn router() -> Router<AppState> {
Router::new()
.route("/emojis", get(handlers::get_all).post(handlers::create))
.route(
"/emojis/{id}",
get(handlers::get_by_id)
.put(handlers::update)
.delete(handlers::delete),
)
.route("/emojis/{id}/asset", get(handlers::asset))
}
+9 -4
View File
@@ -6,6 +6,7 @@ use crate::routes::category::mapper::category_model_to_category_response;
use crate::routes::channel::mapper::channel_model_to_channel_response;
use crate::routes::message::mapper::message_model_to_message_response_with_server_id;
use crate::routes::server::mapper::server_model_to_server_response;
use crate::services::Services;
use axum::extract::ws::Message;
use event_bus::EventBus;
use events::GatewayEvent;
@@ -15,7 +16,6 @@ use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use uuid::Uuid;
use crate::services::Services;
pub mod events;
pub mod handlers;
@@ -44,7 +44,10 @@ pub struct GatewayClient {
impl GatewayManager {
pub fn new(services: Arc<Services>) -> Self {
Self { clients: RwLock::new(HashMap::new()), services }
Self {
clients: RwLock::new(HashMap::new()),
services,
}
}
/// Démarre les routeurs centraux des événements de messages.
pub fn start(self: &Arc<Self>, event_bus: Arc<EventBus>) {
@@ -95,7 +98,6 @@ impl GatewayManager {
pub(crate) fn remove_client(&self, gateway_client: &GatewayClient) {
let key = gateway_client.key();
self.clients.write().remove(&key);
}
fn broadcast_message<T: serde::Serialize>(
@@ -113,7 +115,10 @@ impl GatewayManager {
return;
};
let users = self.services.realtime_registry.users_for_channel(channel_id);
let users = self
.services
.realtime_registry
.users_for_channel(channel_id);
let clients = self.clients.read();
for (key, client) in clients.iter() {
if users.contains(&key.user_id) {
+1 -1
View File
@@ -1,7 +1,7 @@
use super::handlers;
use crate::core::AppState;
use axum::routing::get;
use axum::Router;
use axum::routing::get;
pub fn router() -> Router<AppState> {
Router::new().route("/gateway", get(handlers::ws_handler))
+1 -1
View File
@@ -1,6 +1,6 @@
use super::handlers;
use crate::core::state::AppState;
use axum::{routing::get, Router};
use axum::{Router, routing::get};
pub fn router() -> Router<AppState> {
Router::new()
+2
View File
@@ -9,6 +9,7 @@ pub mod auth;
pub mod category;
pub mod channel;
pub mod core;
pub mod emoji;
pub mod gateway;
pub mod message;
pub mod openapi;
@@ -25,6 +26,7 @@ pub fn router() -> OxRouter {
.merge(role::routes::router())
.merge(message::routes::router())
.merge(user::routes::router())
.merge(emoji::routes::router())
.layer(axum_middleware::from_fn(middleware::require_auth));
// Routes publiques (ou gérant leur propre auth)
+10
View File
@@ -42,6 +42,11 @@ use utoipa::{Modify, OpenApi};
message::handlers::update,
message::handlers::delete,
core::handlers::join,
emoji::handlers::get_all,
emoji::handlers::get_by_id,
emoji::handlers::create,
emoji::handlers::update,
emoji::handlers::delete,
),
components(
schemas(
@@ -70,6 +75,10 @@ use utoipa::{Modify, OpenApi};
crate::domain::dto::message::UpdateMessageRequest,
crate::domain::dto::core::JoinRequest,
ChannelType,
crate::domain::dto::emoji::EmojiResponse,
crate::domain::dto::emoji::CreateEmojiRequest,
crate::domain::dto::emoji::UpdateEmojiRequest,
crate::domain::dto::emoji::EmojiQueryParams,
)
),
modifiers(&SecurityAddon, &ApiPrefixAddon),
@@ -82,6 +91,7 @@ use utoipa::{Modify, OpenApi};
(name = "roles", description = "Gestion des rolees"),
(name = "Messages", description = "Gestion des messages"),
(name = "Core", description = "Endpoints de base (enregistrement, etc.)"),
(name = "Emojis", description = "Gestion des emojis Unicode et personnalisés"),
)
)]
pub struct ApiDoc;
+149 -25
View File
@@ -1,12 +1,18 @@
use crate::core::state::AppState;
use crate::domain::dto::role::{CreateRoleRequest, RoleQueryParams, RoleResponse, UpdateRoleRequest};
use crate::domain::dto::role::{
CreateRoleRequest, RoleQueryParams, RoleResponse, UpdateRoleRequest,
};
use crate::domain::dto::user::UserResponse;
use crate::http::context::CurrentUser;
use crate::http::error::HTTPError;
use crate::permissions::ServerPermission;
use crate::routes::role::mapper;
use crate::routes::user::mapper as user_mapper;
use axum::{Json, extract::{Path, Query, State}, http::StatusCode};
use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
};
use uuid::Uuid;
async fn require_permission(
@@ -44,7 +50,12 @@ pub async fn get_all(
None => state.repositories.role.get_all().await?,
};
Ok(Json(roles.into_iter().map(mapper::role_model_to_role_response).collect()))
Ok(Json(
roles
.into_iter()
.map(mapper::role_model_to_role_response)
.collect(),
))
}
#[utoipa::path(get, path = "/roles/{id}", params(("id" = Uuid, Path)), responses((status = 200, body = RoleResponse), (status = 404)), tag = "Roles")]
@@ -52,7 +63,12 @@ pub async fn get_by_id(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Json<RoleResponse>, HTTPError> {
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?;
let role = state
.repositories
.role
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
Ok(Json(mapper::role_model_to_role_response(role)))
}
@@ -62,12 +78,29 @@ pub async fn create(
State(state): State<AppState>,
Json(payload): Json<CreateRoleRequest>,
) -> Result<(StatusCode, Json<RoleResponse>), HTTPError> {
state.repositories.server.get_by_id(payload.server_id).await?
state
.repositories
.server
.get_by_id(payload.server_id)
.await?
.ok_or(HTTPError::BadRequest("Server not found".to_string()))?;
require_permission(&state, &user, payload.server_id, ServerPermission::MANAGE_ROLES).await?;
require_permission(
&state,
&user,
payload.server_id,
ServerPermission::MANAGE_ROLES,
)
.await?;
let role = state.services.role.create_role(mapper::create_request_to_am(payload)).await?;
Ok((StatusCode::CREATED, Json(mapper::role_model_to_role_response(role))))
let role = state
.services
.role
.create_role(mapper::create_request_to_am(payload))
.await?;
Ok((
StatusCode::CREATED,
Json(mapper::role_model_to_role_response(role)),
))
}
#[utoipa::path(put, path = "/roles/{id}", request_body = UpdateRoleRequest, responses((status = 200, body = RoleResponse), (status = 404)), tag = "Roles", security(("bearerAuth" = [])))]
@@ -77,10 +110,29 @@ pub async fn update(
Path(id): Path<Uuid>,
Json(payload): Json<UpdateRoleRequest>,
) -> Result<Json<RoleResponse>, HTTPError> {
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?;
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_ROLES).await?;
let role = state
.repositories
.role
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
require_permission(
&state,
&user,
role.server_id,
ServerPermission::MANAGE_ROLES,
)
.await?;
let role = state.services.role.update_role(mapper::update_request_to_am(role.id, role.server_id, payload)).await?;
let role = state
.services
.role
.update_role(mapper::update_request_to_am(
role.id,
role.server_id,
payload,
))
.await?;
Ok(Json(mapper::role_model_to_role_response(role)))
}
@@ -90,10 +142,25 @@ pub async fn delete(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, HTTPError> {
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?;
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_ROLES).await?;
let role = state
.repositories
.role
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
require_permission(
&state,
&user,
role.server_id,
ServerPermission::MANAGE_ROLES,
)
.await?;
if state.services.role.delete_role(id).await? { Ok(StatusCode::NO_CONTENT) } else { Err(HTTPError::NotFound) }
if state.services.role.delete_role(id).await? {
Ok(StatusCode::NO_CONTENT)
} else {
Err(HTTPError::NotFound)
}
}
pub async fn get_members(
@@ -101,10 +168,26 @@ pub async fn get_members(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Json<Vec<UserResponse>>, HTTPError> {
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?;
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_MEMBERS).await?;
let role = state
.repositories
.role
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
require_permission(
&state,
&user,
role.server_id,
ServerPermission::MANAGE_MEMBERS,
)
.await?;
let members = state.repositories.role.get_members(id).await?;
Ok(Json(members.into_iter().map(user_mapper::user_model_to_user_response).collect()))
Ok(Json(
members
.into_iter()
.map(user_mapper::user_model_to_user_response)
.collect(),
))
}
pub async fn add_member(
@@ -112,11 +195,32 @@ pub async fn add_member(
State(state): State<AppState>,
Path((id, user_id)): Path<(Uuid, Uuid)>,
) -> Result<StatusCode, HTTPError> {
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?;
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_MEMBERS).await?;
state.repositories.server.get_user(role.server_id, user_id).await?
.ok_or(HTTPError::BadRequest("User is not a member of this server".to_string()))?;
state.services.role.add_member(id, user_id, role.server_id).await?;
let role = state
.repositories
.role
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
require_permission(
&state,
&user,
role.server_id,
ServerPermission::MANAGE_MEMBERS,
)
.await?;
state
.repositories
.server
.get_user(role.server_id, user_id)
.await?
.ok_or(HTTPError::BadRequest(
"User is not a member of this server".to_string(),
))?;
state
.services
.role
.add_member(id, user_id, role.server_id)
.await?;
Ok(StatusCode::NO_CONTENT)
}
@@ -125,7 +229,27 @@ pub async fn remove_member(
State(state): State<AppState>,
Path((id, user_id)): Path<(Uuid, Uuid)>,
) -> Result<StatusCode, HTTPError> {
let role = state.repositories.role.get_by_id(id).await?.ok_or(HTTPError::NotFound)?;
require_permission(&state, &user, role.server_id, ServerPermission::MANAGE_MEMBERS).await?;
if state.services.role.remove_member(id, user_id, role.server_id).await? { Ok(StatusCode::NO_CONTENT) } else { Err(HTTPError::NotFound) }
let role = state
.repositories
.role
.get_by_id(id)
.await?
.ok_or(HTTPError::NotFound)?;
require_permission(
&state,
&user,
role.server_id,
ServerPermission::MANAGE_MEMBERS,
)
.await?;
if state
.services
.role
.remove_member(id, user_id, role.server_id)
.await?
{
Ok(StatusCode::NO_CONTENT)
} else {
Err(HTTPError::NotFound)
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::models::role;
use crate::domain::dto::role::{CreateRoleRequest, RoleResponse, UpdateRoleRequest};
use crate::models::role;
use sea_orm::Set;
use uuid::Uuid;
+8 -2
View File
@@ -1,5 +1,8 @@
use crate::core::state::AppState;
use axum::{routing::{get, put}, Router};
use axum::{
Router,
routing::{get, put},
};
use super::handlers;
@@ -13,5 +16,8 @@ pub fn router() -> Router<AppState> {
.delete(handlers::delete),
)
.route("/roles/{id}/members", get(handlers::get_members))
.route("/roles/{id}/members/{user_id}", put(handlers::add_member).delete(handlers::remove_member))
.route(
"/roles/{id}/members/{user_id}",
put(handlers::add_member).delete(handlers::remove_member),
)
}
+30 -6
View File
@@ -32,7 +32,11 @@ async fn require_server_permission(
.map(|value| ServerPermission::from_bits_truncate(value.permissions as u64))
.unwrap_or_default();
if granted.contains(permission) { Ok(()) } else { Err(HTTPError::Forbidden) }
if granted.contains(permission) {
Ok(())
} else {
Err(HTTPError::Forbidden)
}
}
/// Liste tous les serveurs
@@ -241,9 +245,23 @@ pub async fn list_user_permissions(
State(state): State<AppState>,
Path(server_id): Path<Uuid>,
) -> Result<Json<Vec<ServerUserPermissionResponse>>, HTTPError> {
state.repositories.server.get_by_id(server_id).await?.ok_or(HTTPError::NotFound)?;
let permissions = state.repositories.server.get_user_permissions(server_id).await?;
Ok(Json(permissions.into_iter().map(mapper::server_user_permission_to_response).collect()))
state
.repositories
.server
.get_by_id(server_id)
.await?
.ok_or(HTTPError::NotFound)?;
let permissions = state
.repositories
.server
.get_user_permissions(server_id)
.await?;
Ok(Json(
permissions
.into_iter()
.map(mapper::server_user_permission_to_response)
.collect(),
))
}
/// Définit ou remplace les permissions directes d'un utilisateur sur un serveur.
@@ -268,8 +286,14 @@ pub async fn set_user_permission(
Json(payload): Json<SetServerPermissionRequest>,
) -> Result<Json<ServerUserPermissionResponse>, HTTPError> {
require_server_permission(&state, &user, server_id, ServerPermission::MANAGE_MEMBERS).await?;
state.repositories.server.get_user(server_id, user_id).await?
.ok_or(HTTPError::BadRequest("User is not a member of this server".to_string()))?;
state
.repositories
.server
.get_user(server_id, user_id)
.await?
.ok_or(HTTPError::BadRequest(
"User is not a member of this server".to_string(),
))?;
state
.repositories
.server
+4 -1
View File
@@ -130,7 +130,10 @@ pub fn build_server_tree(
.into_iter()
.map(|c| {
let chan_perm_bits = c.permissions.map(|p| p.bits()).unwrap_or(0);
let mut response = channel_model_to_channel_response_with_permission(c.channel, Some(chan_perm_bits));
let mut response = channel_model_to_channel_response_with_permission(
c.channel,
Some(chan_perm_bits),
);
response.unread_count = Some(*unread_counts.get(&response.id).unwrap_or(&0));
response
})
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::auth::password::hash_password;
use crate::models::user;
use crate::domain::dto::user::{CreateUserRequest, UpdateUserRequest, UserResponse};
use crate::models::user;
use anyhow::Result as AnyResult;
use sea_orm::{NotSet, Set};
use uuid::Uuid;
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::core::state::AppState;
use axum::{routing::get, Router};
use axum::{Router, routing::get};
use super::handlers;