diff --git a/.gitignore b/.gitignore index 40d9aca..65a1d7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -/.idea \ No newline at end of file +/.idea +*.db \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 155dbb8..5dda257 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ uuid = { version = "1.23.1", features = ["v4", "v7", "fast-rng", "serde"] } tracing = "0.1.44" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "time"] } thiserror = "2" -utoipa = { version = "5", features = ["uuid"] } +utoipa = { version = "5", features = ["uuid", "chrono"] } log = "0.4" bitflags = "2.11.1" argon2 = { version = "0.6.0-rc.8", features = ["password-hash"] } diff --git a/src/core/mod.rs b/src/core/mod.rs index 1399df2..6372c48 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -3,12 +3,14 @@ pub mod state; use crate::config::AppConfig; use crate::database::Database; use crate::http::server::HttpServer; +use crate::metrics::{reporter, AppMetrics}; use crate::repositories::Repositories; use crate::udp::server::UdpServer; use event_bus::EventBus; use migration::{Migrator, MigratorTrait}; pub use state::AppState; use std::sync::Arc; +use std::time::Duration; use uuid::Uuid; pub struct App { @@ -58,12 +60,15 @@ impl App { None }; + let metrics = AppMetrics::new(); + let state = AppState { db, config: Arc::new(config), repositories, init_token, default_server: Arc::new(default_server), + metrics, }; Ok(Self { state }) @@ -78,7 +83,14 @@ impl App { let (http_server, http_shutdown_tx) = HttpServer::new(&config.network, self.state.clone()); // Initialize UDP service - let (udp_server, udp_shutdown_tx) = UdpServer::new(&config.network); + let udp_metrics = Arc::clone(&self.state.metrics.udp); + let (udp_server, udp_shutdown_tx) = UdpServer::new(&config.network, udp_metrics); + + // Lance le reporter central de métriques toutes les 30 secondes + reporter::spawn_reporter( + Arc::new(self.state.metrics.clone()), + Duration::from_secs(30), + ); // On lance les serveurs dans des tâches séparées let mut http_handle = tokio::spawn(http_server.run()); diff --git a/src/core/state.rs b/src/core/state.rs index bc61b7e..7d88cd2 100644 --- a/src/core/state.rs +++ b/src/core/state.rs @@ -1,4 +1,5 @@ use crate::config::AppConfig; +use crate::metrics::AppMetrics; use crate::models::server; use crate::repositories::Repositories; use sea_orm::DatabaseConnection; @@ -11,6 +12,7 @@ pub struct AppState { pub repositories: Repositories, pub init_token: Option, pub default_server: Arc, + pub metrics: AppMetrics, } impl AppState {} diff --git a/src/http/context.rs b/src/http/context.rs index 4eb73e2..6d47082 100644 --- a/src/http/context.rs +++ b/src/http/context.rs @@ -66,3 +66,42 @@ where context.user.clone().ok_or(HTTPError::Unauthorized) } } + +/// Représente un utilisateur avec les privilèges d'administrateur. +/// +/// **Usage :** +/// ```rust +/// pub async fn suppression_globale(admin: Superuser) { +/// // Ici, nous sommes certains que admin.is_superuser est true. +/// } +/// ``` +#[derive(Clone, Debug)] +pub struct Superuser(pub user::Model); + +impl Deref for Superuser { + type Target = user::Model; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromRequestParts for Superuser +where + S: Send + Sync, +{ + type Rejection = HTTPError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + // On récupère d'abord l'utilisateur authentifié normalement + let current_user = CurrentUser::from_request_parts(parts, state).await?; + + // On vérifie le flag superuser + if current_user.is_superuser { + Ok(Superuser(current_user.0)) + } else { + // L'utilisateur est authentifié mais n'a pas les droits + Err(HTTPError::Forbidden) + } + } +} diff --git a/src/http/metrics.rs b/src/http/metrics.rs index 4611d20..bf27364 100644 --- a/src/http/metrics.rs +++ b/src/http/metrics.rs @@ -23,6 +23,8 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; +use crate::metrics::{Metrics, MetricsSnapshot}; + // ── Compteurs ──────────────────────────────────────────────────────────────── /// Compteurs atomiques du serveur HTTP. @@ -99,6 +101,14 @@ impl HttpMetrics { } } +impl Metrics for HttpMetrics { + type Snapshot = HttpMetricsSnapshot; + + fn snapshot(&self) -> HttpMetricsSnapshot { + self.snapshot() + } +} + // ── Snapshot ───────────────────────────────────────────────────────────────── /// Lecture cohérente des compteurs à un instant T. @@ -148,6 +158,12 @@ impl HttpMetricsSnapshot { } } +impl MetricsSnapshot for HttpMetricsSnapshot { + fn taken_at(&self) -> Instant { + self.taken_at + } +} + // ── Taux ───────────────────────────────────────────────────────────────────── /// Taux moyens par seconde calculés entre deux snapshots. diff --git a/src/http/server.rs b/src/http/server.rs index 79a6ee5..1ca14fc 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -6,7 +6,6 @@ use std::net::SocketAddr; use std::sync::Arc; -use std::time::Duration; use axum::middleware as axum_middleware; use axum::Router; @@ -16,12 +15,11 @@ use tower_http::catch_panic::CatchPanicLayer; use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; -use crate::config::{AppConfig, NetworkConfig}; +use crate::config::NetworkConfig; use crate::core::AppState; -use crate::http::OxRouter; use crate::routes; -use super::metrics::{self, HttpMetrics}; +use super::metrics::HttpMetrics; use super::middleware::context_middleware; // ── Erreurs ─────────────────────────────────────────────────────────────────── @@ -82,7 +80,7 @@ impl HttpServer { app_state: AppState, ) -> (Self, broadcast::Sender<()>) { let bind_addr = SocketAddr::new(network_config.host.into(), network_config.tcp_port); - let metrics = HttpMetrics::new(); + let metrics = Arc::clone(&app_state.metrics.http); let (shutdown_tx, shutdown_rx) = broadcast::channel(1); ( @@ -106,9 +104,6 @@ impl HttpServer { /// La future se résout lorsqu'un signal de shutdown est reçu ou qu'une /// erreur I/O fatale survient. pub async fn run(mut self) -> Result<(), HttpServerError> { - // Lance le reporter de métriques toutes les 30 secondes - metrics::spawn_reporter(self.metrics.clone(), Duration::from_secs(30)); - let metrics = self.metrics.clone(); let app_state = self.app_state.clone(); diff --git a/src/lib.rs b/src/lib.rs index 021f37e..d9f0858 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,3 +10,4 @@ pub mod routes; pub mod udp; pub mod auth; +pub mod metrics; diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs new file mode 100644 index 0000000..03b8726 --- /dev/null +++ b/src/metrics/mod.rs @@ -0,0 +1,41 @@ +//! Traits communs pour l'uniformisation des métriques. + +use std::sync::Arc; +use std::time::Instant; + +use crate::http::metrics::HttpMetrics; +use crate::udp::metrics::UdpMetrics; + +/// Contrat minimal pour un jeu de compteurs métriques. +pub trait Metrics { + type Snapshot: MetricsSnapshot; + + /// Prend un instantané cohérent des compteurs à l'instant T. + fn snapshot(&self) -> Self::Snapshot; +} + +/// Contrat minimal pour un snapshot de métriques. +pub trait MetricsSnapshot: Clone { + /// Instant auquel le snapshot a été pris. + fn taken_at(&self) -> Instant; +} + +// ── AppMetrics ──────────────────────────────────────────────────────────────── + +/// Regroupe toutes les métriques de l'application. +#[derive(Debug, Clone)] +pub struct AppMetrics { + pub http: Arc, + pub udp: Arc, +} + +impl AppMetrics { + pub fn new() -> Self { + Self { + http: HttpMetrics::new(), + udp: UdpMetrics::new(), + } + } +} + +pub mod reporter; diff --git a/src/metrics/reporter.rs b/src/metrics/reporter.rs new file mode 100644 index 0000000..733e049 --- /dev/null +++ b/src/metrics/reporter.rs @@ -0,0 +1,50 @@ +//! Reporter central — orchestre le snapshot et le logging de toutes les métriques. + +use std::sync::Arc; +use std::time::Duration; + +use crate::metrics::AppMetrics; + +/// Lance une tâche tokio unique qui reporte toutes les métriques à intervalle régulier. +pub fn spawn_reporter(metrics: Arc, interval: Duration) { + let metrics_http = Arc::clone(&metrics.http); + let metrics_udp = Arc::clone(&metrics.udp); + + tokio::spawn(async move { + let mut ticker = tokio::time::interval(interval); + ticker.tick().await; + + let mut prev_http = metrics_http.snapshot(); + let mut prev_udp = metrics_udp.snapshot(); + + loop { + ticker.tick().await; + + let current_http = metrics_http.snapshot(); + let current_udp = metrics_udp.snapshot(); + + let http_rates = current_http.rates_since(&prev_http); + let udp_rates = current_udp.rates_since(&prev_udp); + + tracing::info!( + // ── HTTP ── + http_requests_total = current_http.requests_total, + http_responses_2xx = current_http.responses_2xx, + http_responses_4xx = current_http.responses_4xx, + http_responses_5xx = current_http.responses_5xx, + http_req_per_sec = format_args!("{:.2}", http_rates.requests_per_sec), + http_avg_latency_ms = format_args!("{:.1}", http_rates.avg_latency_ms), + // ── UDP ── + udp_pkts_rx = current_udp.packets_received, + udp_pkts_tx = current_udp.packets_sent, + udp_pkts_dropped = current_udp.packets_dropped, + udp_pkts_rx_s = format_args!("{:.1}", udp_rates.packets_received_per_sec), + udp_pkts_tx_s = format_args!("{:.1}", udp_rates.packets_sent_per_sec), + "App metrics" + ); + + prev_http = current_http; + prev_udp = current_udp; + } + }); +} diff --git a/src/repositories/category.rs b/src/repositories/category.rs index 4922e22..a6b54ed 100644 --- a/src/repositories/category.rs +++ b/src/repositories/category.rs @@ -1,7 +1,8 @@ use crate::models::category; use crate::repositories::{AnyResult, RepositoryContext}; -use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter}; +use sea_orm::{ActiveModelTrait, EntityTrait}; use std::sync::Arc; +use uuid::Uuid; #[derive(Clone, Debug)] pub struct CategoryRepository { @@ -9,7 +10,7 @@ pub struct CategoryRepository { } impl CategoryRepository { - pub async fn get_by_id(&self, id: uuid::Uuid) -> AnyResult> { + pub async fn get_by_id(&self, id: Uuid) -> AnyResult> { Ok(category::Entity::find_by_id(id) .one(&self.context.db) .await?) @@ -19,18 +20,11 @@ impl CategoryRepository { Ok(category::Entity::find().all(&self.context.db).await?) } - pub async fn get_by_server_id(&self, server_id: uuid::Uuid) -> AnyResult> { - Ok(category::Entity::find() - .filter(category::Column::ServerId.eq(server_id)) - .all(&self.context.db) - .await?) - } - pub async fn update(&self, active: category::ActiveModel) -> AnyResult { let category = active.update(&self.context.db).await?; self.context .events - .emit("Category_updated", category.clone()); + .emit("category_updated", category.clone()); Ok(category) } @@ -38,15 +32,15 @@ impl CategoryRepository { let category = active.insert(&self.context.db).await?; self.context .events - .emit("Category_created", category.clone()); + .emit("category_created", category.clone()); Ok(category) } - pub async fn delete(&self, id: uuid::Uuid) -> AnyResult<()> { - category::Entity::delete_by_id(id) + pub async fn delete(&self, id: Uuid) -> AnyResult { + let res = category::Entity::delete_by_id(id) .exec(&self.context.db) .await?; - self.context.events.emit("Category_deleted", id); - Ok(()) + self.context.events.emit("category_deleted", id); + Ok(res.rows_affected > 0) } } diff --git a/src/repositories/channel.rs b/src/repositories/channel.rs index 1696de4..33c89e7 100644 --- a/src/repositories/channel.rs +++ b/src/repositories/channel.rs @@ -15,6 +15,10 @@ impl ChannelRepository { .await?) } + pub async fn get_all(&self) -> AnyResult> { + Ok(channel::Entity::find().all(&self.context.db).await?) + } + pub async fn update(&self, active: channel::ActiveModel) -> AnyResult { let channel = active.update(&self.context.db).await?; self.context.events.emit("channel_updated", channel.clone()); @@ -27,11 +31,11 @@ impl ChannelRepository { Ok(channel) } - pub async fn delete(&self, id: uuid::Uuid) -> AnyResult<()> { - channel::Entity::delete_by_id(id) + pub async fn delete(&self, id: uuid::Uuid) -> AnyResult { + let res = channel::Entity::delete_by_id(id) .exec(&self.context.db) .await?; self.context.events.emit("channel_deleted", id); - Ok(()) + Ok(res.rows_affected > 0) } } diff --git a/src/repositories/group.rs b/src/repositories/group.rs index e358465..bcd1937 100644 --- a/src/repositories/group.rs +++ b/src/repositories/group.rs @@ -17,6 +17,10 @@ impl GroupRepository { .await?) } + pub async fn get_all(&self) -> AnyResult> { + Ok(group::Entity::find().all(&self.context.db).await?) + } + pub async fn get_by_id(&self, id: Uuid) -> AnyResult> { Ok(group::Entity::find_by_id(id).one(&self.context.db).await?) } diff --git a/src/repositories/message.rs b/src/repositories/message.rs index d839f2c..cb27e4b 100644 --- a/src/repositories/message.rs +++ b/src/repositories/message.rs @@ -9,6 +9,10 @@ pub struct MessageRepository { } impl MessageRepository { + pub async fn get_all(&self) -> AnyResult> { + Ok(message::Entity::find().all(&self.context.db).await?) + } + pub async fn get_by_id(&self, id: uuid::Uuid) -> AnyResult> { Ok(message::Entity::find_by_id(id) .one(&self.context.db) @@ -27,11 +31,14 @@ impl MessageRepository { Ok(message) } - pub async fn delete(&self, id: uuid::Uuid) -> AnyResult<()> { - message::Entity::delete_by_id(id) + pub async fn delete(&self, id: uuid::Uuid) -> AnyResult { + let result = message::Entity::delete_by_id(id) .exec(&self.context.db) .await?; - self.context.events.emit("message_deleted", id); - Ok(()) + let deleted = result.rows_affected > 0; + if deleted { + self.context.events.emit("message_deleted", id); + } + Ok(deleted) } } diff --git a/src/repositories/server.rs b/src/repositories/server.rs index 1458da7..a9c2d4c 100644 --- a/src/repositories/server.rs +++ b/src/repositories/server.rs @@ -64,7 +64,7 @@ impl ServerRepository { } pub async fn add_user(&self, server_id: Uuid, user_id: Uuid) -> AnyResult { - let res = server_user::ActiveModel { + server_user::ActiveModel { server_id: Set(server_id), user_id: Set(user_id), ..Default::default() diff --git a/src/repositories/user.rs b/src/repositories/user.rs index d166e60..ddadafc 100644 --- a/src/repositories/user.rs +++ b/src/repositories/user.rs @@ -73,12 +73,15 @@ impl UserRepository { Ok(()) } - pub async fn delete(&self, id: uuid::Uuid) -> AnyResult<()> { - user::Entity::delete_by_id(id) + pub async fn delete(&self, id: uuid::Uuid) -> AnyResult { + let result = user::Entity::delete_by_id(id) .exec(&self.context.db) .await?; - self.context.events.emit("user_deleted", id); - Ok(()) + let deleted = result.rows_affected > 0; + if deleted { + self.context.events.emit("user_deleted", id); + } + Ok(deleted) } pub async fn username_exists(&self, username: &str) -> AnyResult { diff --git a/src/routes/auth/handlers.rs b/src/routes/auth/handlers.rs index dbf5571..ea55124 100644 --- a/src/routes/auth/handlers.rs +++ b/src/routes/auth/handlers.rs @@ -48,8 +48,8 @@ pub async fn login_user_pw( ) )] pub async fn check( - State(state): State, - user: CurrentUser, + State(_state): State, + _user: CurrentUser, ) -> Result, HTTPError> { Ok(Json(CheckResponse { authenticated: true, diff --git a/src/routes/category/domain.rs b/src/routes/category/domain.rs index 483dcd0..a7329f8 100644 --- a/src/routes/category/domain.rs +++ b/src/routes/category/domain.rs @@ -1 +1,2 @@ -pub struct Category {} +// Ce fichier est conservé pour structure mais n'est plus utilisé. +// Les modèles de domaine sont directement gérés par SeaORM dans src/models. diff --git a/src/routes/category/dto.rs b/src/routes/category/dto.rs index 1cc8a3c..bbb193e 100644 --- a/src/routes/category/dto.rs +++ b/src/routes/category/dto.rs @@ -1,10 +1,30 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; -#[derive(Debug, Serialize, Deserialize)] -pub struct CreateCategoryRequest {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct CreateCategoryRequest { + pub server_id: Uuid, + #[schema(example = "Discussion")] + pub name: String, + #[serde(default)] + pub position: i32, +} -#[derive(Debug, Serialize, Deserialize)] -pub struct UpdateCategoryRequest {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct UpdateCategoryRequest { + #[schema(example = "Discussion (Maj)")] + pub name: String, + pub position: i32, +} -#[derive(Debug, Serialize, Deserialize)] -pub struct CategoryResponse {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct CategoryResponse { + pub id: Uuid, + pub server_id: Uuid, + pub name: String, + pub position: i32, + pub created_at: DateTime, + pub updated_at: DateTime, +} diff --git a/src/routes/category/handlers.rs b/src/routes/category/handlers.rs index dfb714a..71ddd0b 100644 --- a/src/routes/category/handlers.rs +++ b/src/routes/category/handlers.rs @@ -1,22 +1,157 @@ -use axum::http::StatusCode; -use axum::response::IntoResponse; +use crate::core::state::AppState; +use crate::http::context::Superuser; +use crate::http::error::HTTPError; +use crate::routes::category::dto::{ + CategoryResponse, CreateCategoryRequest, UpdateCategoryRequest, +}; +use crate::routes::category::mapper; +use axum::{ + extract::{Path, State}, + http::StatusCode, + Json, +}; +use uuid::Uuid; -pub async fn get_all() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Liste toutes les catégories +#[utoipa::path( + get, + path = "/categories", + responses( + (status = 200, description = "Liste des catégories récupérée avec succès", body = [CategoryResponse]), + (status = 500, description = "Erreur interne du serveur") + ), + tag = "Categories" +)] +pub async fn get_all( + State(state): State, +) -> Result>, HTTPError> { + let categories = state.repositories.category.get_all().await?; + Ok(Json( + categories + .into_iter() + .map(mapper::category_model_to_category_response) + .collect(), + )) } -pub async fn get_by_id() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Récupère une catégorie par son ID +#[utoipa::path( + get, + path = "/categories/{id}", + responses( + (status = 200, description = "Catégorie trouvée", body = CategoryResponse), + (status = 404, description = "Catégorie non trouvée"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID de la catégorie") + ), + tag = "Categories" +)] +pub async fn get_by_id( + State(state): State, + Path(id): Path, +) -> Result, HTTPError> { + let category = state + .repositories + .category + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + Ok(Json(mapper::category_model_to_category_response(category))) } -pub async fn create() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Crée une nouvelle catégorie +#[utoipa::path( + post, + path = "/categories", + request_body = CreateCategoryRequest, + responses( + (status = 201, description = "Catégorie créée avec succès", body = CategoryResponse), + (status = 404, description = "Serveur non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + tag = "Categories" +)] +pub async fn create( + _admin: Superuser, + State(state): State, + Json(payload): Json, +) -> Result<(StatusCode, Json), HTTPError> { + // Vérifier que le serveur existe + state + .repositories + .server + .get_by_id(payload.server_id) + .await? + .ok_or(HTTPError::BadRequest("Server not found".to_string()))?; + + let active_model = mapper::create_request_to_am(payload); + let category = state.repositories.category.create(active_model).await?; + Ok(( + StatusCode::CREATED, + Json(mapper::category_model_to_category_response(category)), + )) } -pub async fn update() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Met à jour une catégorie existante +#[utoipa::path( + put, + path = "/categories/{id}", + request_body = UpdateCategoryRequest, + responses( + (status = 200, description = "Catégorie mise à jour avec succès", body = CategoryResponse), + (status = 404, description = "Catégorie non trouvée"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID de la catégorie") + ), + tag = "Categories" +)] +pub async fn update( + _admin: Superuser, + State(state): State, + Path(id): Path, + Json(payload): Json, +) -> Result, HTTPError> { + // Vérifier l'existence + let category = state + .repositories + .category + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + let active_model = mapper::update_request_to_am(category.id, category.server_id, payload); + let category = state.repositories.category.update(active_model).await?; + + Ok(Json(mapper::category_model_to_category_response(category))) } -pub async fn delete() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Supprime une catégorie +#[utoipa::path( + delete, + path = "/categories/{id}", + responses( + (status = 204, description = "Catégorie supprimée avec succès"), + (status = 404, description = "Catégorie non trouvée"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID de la catégorie") + ), + tag = "Categories" +)] +pub async fn delete( + _admin: Superuser, + State(state): State, + Path(id): Path, +) -> Result { + if state.repositories.category.delete(id).await? { + Ok(StatusCode::NO_CONTENT) + } else { + Err(HTTPError::NotFound) + } } diff --git a/src/routes/category/mapper.rs b/src/routes/category/mapper.rs index 4336f3f..6d3d97d 100644 --- a/src/routes/category/mapper.rs +++ b/src/routes/category/mapper.rs @@ -1,5 +1,41 @@ -use super::{domain::Category, dto::CategoryResponse}; +use crate::models::category; +use crate::routes::category::dto::{ + CategoryResponse, CreateCategoryRequest, UpdateCategoryRequest, +}; +use sea_orm::Set; +use uuid::Uuid; -pub fn to_response(_item: Category) -> CategoryResponse { - todo!() +pub fn category_model_to_category_response(model: category::Model) -> CategoryResponse { + CategoryResponse { + id: model.id, + server_id: model.server_id, + name: model.name, + position: model.position, + created_at: model.created_at, + updated_at: model.updated_at, + } +} + +pub fn create_request_to_am(req: CreateCategoryRequest) -> category::ActiveModel { + category::ActiveModel { + id: Set(Uuid::new_v4()), + server_id: Set(req.server_id), + name: Set(req.name), + position: Set(req.position), + ..Default::default() + } +} + +pub fn update_request_to_am( + id: Uuid, + server_id: Uuid, + req: UpdateCategoryRequest, +) -> category::ActiveModel { + category::ActiveModel { + id: Set(id), + server_id: Set(server_id), + name: Set(req.name), + position: Set(req.position), + ..Default::default() + } } diff --git a/src/routes/category/routes.rs b/src/routes/category/routes.rs index 4eaccac..5583221 100644 --- a/src/routes/category/routes.rs +++ b/src/routes/category/routes.rs @@ -1,12 +1,13 @@ -use axum::{Router, routing::get}; +use crate::core::state::AppState; +use axum::{routing::get, Router}; use super::handlers; -pub fn router() -> Router { +pub fn router() -> Router { Router::new() - .route("/categorys", get(handlers::get_all).post(handlers::create)) + .route("/categories", get(handlers::get_all).post(handlers::create)) .route( - "/categorys/:id", + "/categories/:id", get(handlers::get_by_id) .put(handlers::update) .delete(handlers::delete), diff --git a/src/routes/category/service.rs b/src/routes/category/service.rs index e1597a6..cc5e02f 100644 --- a/src/routes/category/service.rs +++ b/src/routes/category/service.rs @@ -1,21 +1,2 @@ -use super::domain::Category; - -pub async fn find_all() -> Vec { - todo!() -} - -pub async fn find_by_id(_id: u64) -> Option { - todo!() -} - -pub async fn create(_item: Category) -> Category { - todo!() -} - -pub async fn update(_id: u64, _item: Category) -> Option { - todo!() -} - -pub async fn delete(_id: u64) -> bool { - todo!() -} +// Ce fichier est conservé pour structure mais n'est plus utilisé. +// La logique a été déplacée directement dans les handlers. diff --git a/src/routes/channel/domain.rs b/src/routes/channel/domain.rs index 9984429..a7329f8 100644 --- a/src/routes/channel/domain.rs +++ b/src/routes/channel/domain.rs @@ -1 +1,2 @@ -pub struct Channel {} +// Ce fichier est conservé pour structure mais n'est plus utilisé. +// Les modèles de domaine sont directement gérés par SeaORM dans src/models. diff --git a/src/routes/channel/dto.rs b/src/routes/channel/dto.rs index 5f0fe50..1e3f8f7 100644 --- a/src/routes/channel/dto.rs +++ b/src/routes/channel/dto.rs @@ -1,10 +1,40 @@ +use crate::models::channel::ChannelType; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; -#[derive(Debug, Serialize, Deserialize)] -pub struct CreateChannelRequest {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct CreateChannelRequest { + pub server_id: Option, + pub category_id: Option, + #[serde(default)] + pub position: i32, + pub channel_type: ChannelType, + #[schema(example = "général")] + pub name: Option, + pub default_permissions: Option, +} -#[derive(Debug, Serialize, Deserialize)] -pub struct UpdateChannelRequest {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct UpdateChannelRequest { + pub server_id: Option, + pub category_id: Option, + pub position: i32, + pub channel_type: ChannelType, + pub name: Option, + pub default_permissions: Option, +} -#[derive(Debug, Serialize, Deserialize)] -pub struct ChannelResponse {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct ChannelResponse { + pub id: Uuid, + pub server_id: Option, + pub category_id: Option, + pub position: i32, + pub channel_type: ChannelType, + pub name: Option, + pub created_at: DateTime, + pub updated_at: DateTime, + pub default_permissions: Option, +} diff --git a/src/routes/channel/handlers.rs b/src/routes/channel/handlers.rs index dfb714a..46ec8c5 100644 --- a/src/routes/channel/handlers.rs +++ b/src/routes/channel/handlers.rs @@ -1,22 +1,188 @@ -use axum::http::StatusCode; -use axum::response::IntoResponse; +use crate::core::state::AppState; +use crate::http::context::Superuser; +use crate::http::error::HTTPError; +use crate::routes::channel::dto::{ChannelResponse, CreateChannelRequest, UpdateChannelRequest}; +use crate::routes::channel::mapper; +use axum::{ + extract::{Path, State}, + http::StatusCode, + Json, +}; +use uuid::Uuid; -pub async fn get_all() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Liste tous les channels +#[utoipa::path( + get, + path = "/channels", + responses( + (status = 200, description = "Liste des channels récupérée avec succès", body = [ChannelResponse]), + (status = 500, description = "Erreur interne du serveur") + ), + tag = "Channels" +)] +pub async fn get_all( + State(state): State, +) -> Result>, HTTPError> { + let channels = state.repositories.channel.get_all().await?; + Ok(Json( + channels + .into_iter() + .map(mapper::channel_model_to_channel_response) + .collect(), + )) } -pub async fn get_by_id() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Récupère un channel par son ID +#[utoipa::path( + get, + path = "/channels/{id}", + responses( + (status = 200, description = "Channel trouvé", body = ChannelResponse), + (status = 404, description = "Channel non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID du channel") + ), + tag = "Channels" +)] +pub async fn get_by_id( + State(state): State, + Path(id): Path, +) -> Result, HTTPError> { + let channel = state + .repositories + .channel + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + Ok(Json(mapper::channel_model_to_channel_response(channel))) } -pub async fn create() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Crée un nouveau channel +#[utoipa::path( + post, + path = "/channels", + request_body = CreateChannelRequest, + responses( + (status = 201, description = "Channel créé avec succès", body = ChannelResponse), + (status = 400, description = "Données invalides (Serveur ou Catégorie non trouvée)"), + (status = 500, description = "Erreur interne du serveur") + ), + tag = "Channels" +)] +pub async fn create( + _admin: Superuser, + State(state): State, + Json(payload): Json, +) -> Result<(StatusCode, Json), HTTPError> { + // Vérifier que le serveur existe si fourni + if let Some(server_id) = payload.server_id { + state + .repositories + .server + .get_by_id(server_id) + .await? + .ok_or(HTTPError::BadRequest("Server not found".to_string()))?; + } + + // Vérifier que la catégorie existe si fournie + if let Some(category_id) = payload.category_id { + state + .repositories + .category + .get_by_id(category_id) + .await? + .ok_or(HTTPError::BadRequest("Category not found".to_string()))?; + } + + let active_model = mapper::create_request_to_am(payload); + let channel = state.repositories.channel.create(active_model).await?; + Ok(( + StatusCode::CREATED, + Json(mapper::channel_model_to_channel_response(channel)), + )) } -pub async fn update() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Met à jour un channel existant +#[utoipa::path( + put, + path = "/channels/{id}", + request_body = UpdateChannelRequest, + responses( + (status = 200, description = "Channel mis à jour avec succès", body = ChannelResponse), + (status = 404, description = "Channel non trouvé"), + (status = 400, description = "Données invalides (Serveur ou Catégorie non trouvée)"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID du channel") + ), + tag = "Channels" +)] +pub async fn update( + _admin: Superuser, + State(state): State, + Path(id): Path, + Json(payload): Json, +) -> Result, HTTPError> { + // Vérifier l'existence + state + .repositories + .channel + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + // Vérifier que le serveur existe si fourni + if let Some(server_id) = payload.server_id { + state + .repositories + .server + .get_by_id(server_id) + .await? + .ok_or(HTTPError::BadRequest("Server not found".to_string()))?; + } + + // Vérifier que la catégorie existe si fournie + if let Some(category_id) = payload.category_id { + state + .repositories + .category + .get_by_id(category_id) + .await? + .ok_or(HTTPError::BadRequest("Category not found".to_string()))?; + } + + let active_model = mapper::update_request_to_am(id, payload); + let channel = state.repositories.channel.update(active_model).await?; + + Ok(Json(mapper::channel_model_to_channel_response(channel))) } -pub async fn delete() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Supprime un channel +#[utoipa::path( + delete, + path = "/channels/{id}", + responses( + (status = 204, description = "Channel supprimé avec succès"), + (status = 404, description = "Channel non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID du channel") + ), + tag = "Channels" +)] +pub async fn delete( + _admin: Superuser, + State(state): State, + Path(id): Path, +) -> Result { + if state.repositories.channel.delete(id).await? { + Ok(StatusCode::NO_CONTENT) + } else { + Err(HTTPError::NotFound) + } } diff --git a/src/routes/channel/mapper.rs b/src/routes/channel/mapper.rs index e9a120e..8f23fc8 100644 --- a/src/routes/channel/mapper.rs +++ b/src/routes/channel/mapper.rs @@ -1,5 +1,44 @@ -use super::{domain::Channel, dto::ChannelResponse}; +use crate::models::channel; +use crate::routes::channel::dto::{ChannelResponse, CreateChannelRequest, UpdateChannelRequest}; +use sea_orm::Set; +use uuid::Uuid; -pub fn to_response(_item: Channel) -> ChannelResponse { - todo!() +pub fn channel_model_to_channel_response(model: channel::Model) -> ChannelResponse { + ChannelResponse { + id: model.id, + server_id: model.server_id, + category_id: model.category_id, + position: model.position, + channel_type: model.channel_type, + name: model.name, + created_at: model.created_at, + updated_at: model.updated_at, + default_permissions: model.default_permissions, + } +} + +pub fn create_request_to_am(req: CreateChannelRequest) -> channel::ActiveModel { + channel::ActiveModel { + id: Set(Uuid::new_v4()), + server_id: Set(req.server_id), + category_id: Set(req.category_id), + position: Set(req.position), + channel_type: Set(req.channel_type), + name: Set(req.name), + default_permissions: Set(req.default_permissions), + ..Default::default() + } +} + +pub fn update_request_to_am(id: Uuid, req: UpdateChannelRequest) -> channel::ActiveModel { + channel::ActiveModel { + id: Set(id), + server_id: Set(req.server_id), + category_id: Set(req.category_id), + position: Set(req.position), + channel_type: Set(req.channel_type), + name: Set(req.name), + default_permissions: Set(req.default_permissions), + ..Default::default() + } } diff --git a/src/routes/channel/routes.rs b/src/routes/channel/routes.rs index 0cb2e83..d6e533a 100644 --- a/src/routes/channel/routes.rs +++ b/src/routes/channel/routes.rs @@ -1,8 +1,8 @@ -use axum::{Router, routing::get}; - use super::handlers; +use crate::core::state::AppState; +use axum::{routing::get, Router}; -pub fn router() -> Router { +pub fn router() -> Router { Router::new() .route("/channels", get(handlers::get_all).post(handlers::create)) .route( diff --git a/src/routes/channel/service.rs b/src/routes/channel/service.rs index 27731fd..cc5e02f 100644 --- a/src/routes/channel/service.rs +++ b/src/routes/channel/service.rs @@ -1,21 +1,2 @@ -use super::domain::Channel; - -pub async fn find_all() -> Vec { - todo!() -} - -pub async fn find_by_id(_id: u64) -> Option { - todo!() -} - -pub async fn create(_item: Channel) -> Channel { - todo!() -} - -pub async fn update(_id: u64, _item: Channel) -> Option { - todo!() -} - -pub async fn delete(_id: u64) -> bool { - todo!() -} +// Ce fichier est conservé pour structure mais n'est plus utilisé. +// La logique a été déplacée directement dans les handlers. diff --git a/src/routes/group/domain.rs b/src/routes/group/domain.rs index b2ea9a9..a7329f8 100644 --- a/src/routes/group/domain.rs +++ b/src/routes/group/domain.rs @@ -1 +1,2 @@ -pub struct Group {} +// Ce fichier est conservé pour structure mais n'est plus utilisé. +// Les modèles de domaine sont directement gérés par SeaORM dans src/models. diff --git a/src/routes/group/dto.rs b/src/routes/group/dto.rs index a6de477..1ffc5ab 100644 --- a/src/routes/group/dto.rs +++ b/src/routes/group/dto.rs @@ -1,10 +1,38 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; -#[derive(Debug, Serialize, Deserialize)] -pub struct CreateGroupRequest {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct CreateGroupRequest { + pub server_id: Uuid, + #[schema(example = "Modérateurs")] + pub name: String, + #[serde(default)] + pub is_default: bool, + pub server_permissions: i64, + pub channel_permissions: i64, + pub voice_permissions: i64, +} -#[derive(Debug, Serialize, Deserialize)] -pub struct UpdateGroupRequest {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct UpdateGroupRequest { + #[schema(example = "Modérateurs (MAJ)")] + pub name: String, + pub is_default: bool, + pub server_permissions: i64, + pub channel_permissions: i64, + pub voice_permissions: i64, +} -#[derive(Debug, Serialize, Deserialize)] -pub struct GroupResponse {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct GroupResponse { + pub id: Uuid, + pub server_id: Uuid, + pub name: String, + pub is_default: bool, + pub created_at: DateTime, + pub server_permissions: i64, + pub channel_permissions: i64, + pub voice_permissions: i64, +} diff --git a/src/routes/group/handlers.rs b/src/routes/group/handlers.rs index dfb714a..6a952ec 100644 --- a/src/routes/group/handlers.rs +++ b/src/routes/group/handlers.rs @@ -1,22 +1,153 @@ -use axum::http::StatusCode; -use axum::response::IntoResponse; +use crate::core::state::AppState; +use crate::http::context::Superuser; +use crate::http::error::HTTPError; +use crate::routes::group::dto::{CreateGroupRequest, GroupResponse, UpdateGroupRequest}; +use crate::routes::group::mapper; +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, +}; +use uuid::Uuid; -pub async fn get_all() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Liste tous les groupes +#[utoipa::path( + get, + path = "/groups", + responses( + (status = 200, description = "Liste des groupes récupérée avec succès", body = [GroupResponse]), + (status = 500, description = "Erreur interne du serveur") + ), + tag = "Groups" +)] +pub async fn get_all(State(state): State) -> Result>, HTTPError> { + let groups = state.repositories.group.get_all().await?; + Ok(Json( + groups + .into_iter() + .map(mapper::group_model_to_group_response) + .collect(), + )) } -pub async fn get_by_id() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Récupère un groupe par son ID +#[utoipa::path( + get, + path = "/groups/{id}", + responses( + (status = 200, description = "Groupe trouvé", body = GroupResponse), + (status = 404, description = "Groupe non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID du groupe") + ), + tag = "Groups" +)] +pub async fn get_by_id( + State(state): State, + Path(id): Path, +) -> Result, HTTPError> { + let group = state + .repositories + .group + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + Ok(Json(mapper::group_model_to_group_response(group))) } -pub async fn create() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Crée un nouveau groupe +#[utoipa::path( + post, + path = "/groups", + request_body = CreateGroupRequest, + responses( + (status = 201, description = "Groupe créé avec succès", body = GroupResponse), + (status = 404, description = "Serveur non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + tag = "Groups" +)] +pub async fn create( + _admin: Superuser, + State(state): State, + Json(payload): Json, +) -> Result<(StatusCode, Json), HTTPError> { + // Vérifier que le serveur existe + state + .repositories + .server + .get_by_id(payload.server_id) + .await? + .ok_or(HTTPError::BadRequest("Server not found".to_string()))?; + + let active_model = mapper::create_request_to_am(payload); + let group = state.repositories.group.create(active_model).await?; + Ok(( + StatusCode::CREATED, + Json(mapper::group_model_to_group_response(group)), + )) } -pub async fn update() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Met à jour un groupe existant +#[utoipa::path( + put, + path = "/groups/{id}", + request_body = UpdateGroupRequest, + responses( + (status = 200, description = "Groupe mis à jour avec succès", body = GroupResponse), + (status = 404, description = "Groupe non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID du groupe") + ), + tag = "Groups" +)] +pub async fn update( + _admin: Superuser, + State(state): State, + Path(id): Path, + Json(payload): Json, +) -> Result, HTTPError> { + // Vérifier l'existence + let group = state + .repositories + .group + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + let active_model = mapper::update_request_to_am(group.id, group.server_id, payload); + let group = state.repositories.group.update(active_model).await?; + + Ok(Json(mapper::group_model_to_group_response(group))) } -pub async fn delete() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Supprime un groupe +#[utoipa::path( + delete, + path = "/groups/{id}", + responses( + (status = 204, description = "Groupe supprimé avec succès"), + (status = 404, description = "Groupe non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID du groupe") + ), + tag = "Groups" +)] +pub async fn delete( + _admin: Superuser, + State(state): State, + Path(id): Path, +) -> Result { + if state.repositories.group.delete(id).await? { + Ok(StatusCode::NO_CONTENT) + } else { + Err(HTTPError::NotFound) + } } diff --git a/src/routes/group/mapper.rs b/src/routes/group/mapper.rs index 3c5f014..b2288be 100644 --- a/src/routes/group/mapper.rs +++ b/src/routes/group/mapper.rs @@ -1,5 +1,47 @@ -use super::{domain::Group, dto::GroupResponse}; +use crate::models::group; +use crate::routes::group::dto::{CreateGroupRequest, GroupResponse, UpdateGroupRequest}; +use sea_orm::Set; +use uuid::Uuid; -pub fn to_response(_item: Group) -> GroupResponse { - todo!() +pub fn group_model_to_group_response(model: group::Model) -> GroupResponse { + GroupResponse { + id: model.id, + server_id: model.server_id, + name: model.name, + is_default: model.is_default, + created_at: model.created_at, + server_permissions: model.server_permissions, + channel_permissions: model.channel_permissions, + voice_permissions: model.voice_permissions, + } +} + +pub fn create_request_to_am(req: CreateGroupRequest) -> group::ActiveModel { + group::ActiveModel { + id: Set(Uuid::new_v4()), + server_id: Set(req.server_id), + name: Set(req.name), + is_default: Set(req.is_default), + server_permissions: Set(req.server_permissions), + channel_permissions: Set(req.channel_permissions), + voice_permissions: Set(req.voice_permissions), + ..Default::default() + } +} + +pub fn update_request_to_am( + id: Uuid, + server_id: Uuid, + req: UpdateGroupRequest, +) -> group::ActiveModel { + group::ActiveModel { + id: Set(id), + server_id: Set(server_id), + name: Set(req.name), + is_default: Set(req.is_default), + server_permissions: Set(req.server_permissions), + channel_permissions: Set(req.channel_permissions), + voice_permissions: Set(req.voice_permissions), + ..Default::default() + } } diff --git a/src/routes/group/routes.rs b/src/routes/group/routes.rs index c922d72..c397ddf 100644 --- a/src/routes/group/routes.rs +++ b/src/routes/group/routes.rs @@ -1,8 +1,9 @@ -use axum::{Router, routing::get}; +use crate::core::state::AppState; +use axum::{routing::get, Router}; use super::handlers; -pub fn router() -> Router { +pub fn router() -> Router { Router::new() .route("/groups", get(handlers::get_all).post(handlers::create)) .route( diff --git a/src/routes/group/service.rs b/src/routes/group/service.rs index 903dc0f..cc5e02f 100644 --- a/src/routes/group/service.rs +++ b/src/routes/group/service.rs @@ -1,21 +1,2 @@ -use super::domain::Group; - -pub async fn find_all() -> Vec { - todo!() -} - -pub async fn find_by_id(_id: u64) -> Option { - todo!() -} - -pub async fn create(_item: Group) -> Group { - todo!() -} - -pub async fn update(_id: u64, _item: Group) -> Option { - todo!() -} - -pub async fn delete(_id: u64) -> bool { - todo!() -} +// Ce fichier est conservé pour structure mais n'est plus utilisé. +// La logique a été déplacée directement dans les handlers. diff --git a/src/routes/message/domain.rs b/src/routes/message/domain.rs index 1a8f629..6a7e1f1 100644 --- a/src/routes/message/domain.rs +++ b/src/routes/message/domain.rs @@ -1 +1 @@ -pub struct Message {} +// Domain model for Message - currently unused in favor of models::message diff --git a/src/routes/message/dto.rs b/src/routes/message/dto.rs index e94f65f..9546764 100644 --- a/src/routes/message/dto.rs +++ b/src/routes/message/dto.rs @@ -1,10 +1,27 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; -#[derive(Debug, Serialize, Deserialize)] -pub struct CreateMessageRequest {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct MessageResponse { + pub id: Uuid, + pub channel_id: Uuid, + pub user_id: Uuid, + pub content: String, + pub created_at: DateTime, + pub updated_at: Option>, + pub reply_to_id: Option, +} -#[derive(Debug, Serialize, Deserialize)] -pub struct UpdateMessageRequest {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct CreateMessageRequest { + pub channel_id: Uuid, + pub content: String, + pub reply_to_id: Option, +} -#[derive(Debug, Serialize, Deserialize)] -pub struct MessageResponse {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct UpdateMessageRequest { + pub content: String, +} diff --git a/src/routes/message/handlers.rs b/src/routes/message/handlers.rs index dfb714a..7421c68 100644 --- a/src/routes/message/handlers.rs +++ b/src/routes/message/handlers.rs @@ -1,22 +1,187 @@ -use axum::http::StatusCode; -use axum::response::IntoResponse; +use crate::core::state::AppState; +use crate::http::context::CurrentUser; +use crate::http::error::HTTPError; +use crate::routes::message::dto::{CreateMessageRequest, MessageResponse, UpdateMessageRequest}; +use crate::routes::message::mapper; +use axum::{ + extract::{Path, State}, + http::StatusCode, + Json, +}; +use uuid::Uuid; -pub async fn get_all() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Liste tous les messages +#[utoipa::path( + get, + path = "/messages", + responses( + (status = 200, description = "Liste des messages récupérée avec succès", body = [MessageResponse]), + (status = 500, description = "Erreur interne du serveur") + ), + tag = "Messages" +)] +pub async fn get_all( + State(state): State, +) -> Result>, HTTPError> { + let messages = state.repositories.message.get_all().await?; + Ok(Json( + messages + .into_iter() + .map(mapper::message_model_to_message_response) + .collect(), + )) } -pub async fn get_by_id() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Récupère un message par son ID +#[utoipa::path( + get, + path = "/messages/{id}", + responses( + (status = 200, description = "Message trouvé", body = MessageResponse), + (status = 404, description = "Message non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID du message") + ), + tag = "Messages" +)] +pub async fn get_by_id( + State(state): State, + Path(id): Path, +) -> Result, HTTPError> { + let message = state + .repositories + .message + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + Ok(Json(mapper::message_model_to_message_response(message))) } -pub async fn create() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Crée un nouveau message +#[utoipa::path( + post, + path = "/messages", + request_body = CreateMessageRequest, + responses( + (status = 201, description = "Message créé avec succès", body = MessageResponse), + (status = 400, description = "Données invalides (canal non trouvé)"), + (status = 500, description = "Erreur interne du serveur") + ), + tag = "Messages" +)] +pub async fn create( + user: CurrentUser, + State(state): State, + Json(payload): Json, +) -> Result<(StatusCode, Json), HTTPError> { + // Vérifier que le canal existe + state + .repositories + .channel + .get_by_id(payload.channel_id) + .await? + .ok_or(HTTPError::BadRequest("Channel not found".to_string()))?; + + // Optionnel: vérifier reply_to_id + if let Some(reply_id) = payload.reply_to_id { + state + .repositories + .message + .get_by_id(reply_id) + .await? + .ok_or(HTTPError::BadRequest( + "Parent message not found".to_string(), + ))?; + } + + let active_model = mapper::create_request_to_am(user.id, payload); + let message = state.repositories.message.create(active_model).await?; + Ok(( + StatusCode::CREATED, + Json(mapper::message_model_to_message_response(message)), + )) } -pub async fn update() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Met à jour un message existant +#[utoipa::path( + put, + path = "/messages/{id}", + request_body = UpdateMessageRequest, + responses( + (status = 200, description = "Message mis à jour avec succès", body = MessageResponse), + (status = 403, description = "Interdit (pas l'auteur)"), + (status = 404, description = "Message non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID du message") + ), + tag = "Messages" +)] +pub async fn update( + user: CurrentUser, + State(state): State, + Path(id): Path, + Json(payload): Json, +) -> Result, HTTPError> { + // Vérifier l'existence + let message = state + .repositories + .message + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + // Vérifier que l'utilisateur est l'auteur + if message.user_id != user.id && !user.is_superuser { + return Err(HTTPError::Forbidden); + } + + let active_model = mapper::update_request_to_am(message, payload); + let message = state.repositories.message.update(active_model).await?; + + Ok(Json(mapper::message_model_to_message_response(message))) } -pub async fn delete() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Supprime un message +#[utoipa::path( + delete, + path = "/messages/{id}", + responses( + (status = 204, description = "Message supprimé avec succès"), + (status = 403, description = "Interdit (pas l'auteur ou admin)"), + (status = 404, description = "Message non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID du message") + ), + tag = "Messages" +)] +pub async fn delete( + user: CurrentUser, + State(state): State, + Path(id): Path, +) -> Result { + // Vérifier l'existence pour l'autorisation + let message = state + .repositories + .message + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + // Autoriser si auteur ou superuser + if message.user_id != user.id && !user.is_superuser { + return Err(HTTPError::Forbidden); + } + + if state.repositories.message.delete(id).await? { + Ok(StatusCode::NO_CONTENT) + } else { + Err(HTTPError::NotFound) + } } diff --git a/src/routes/message/mapper.rs b/src/routes/message/mapper.rs index d121559..c2fde94 100644 --- a/src/routes/message/mapper.rs +++ b/src/routes/message/mapper.rs @@ -1,5 +1,44 @@ -use super::{domain::Message, dto::MessageResponse}; +use crate::models::message; +use crate::routes::message::dto::{CreateMessageRequest, MessageResponse, UpdateMessageRequest}; +use chrono::Utc; +use sea_orm::Set; +use uuid::Uuid; -pub fn to_response(_item: Message) -> MessageResponse { - todo!() +pub fn message_model_to_message_response(model: message::Model) -> MessageResponse { + MessageResponse { + id: model.id, + channel_id: model.channel_id, + user_id: model.user_id, + content: model.content, + created_at: model.created_at, + updated_at: model.updated_at, + reply_to_id: model.reply_to_id, + } +} + +pub fn create_request_to_am(user_id: Uuid, payload: CreateMessageRequest) -> message::ActiveModel { + message::ActiveModel { + id: Set(Uuid::now_v7()), + channel_id: Set(payload.channel_id), + user_id: Set(user_id), + content: Set(payload.content), + created_at: Set(Utc::now()), + updated_at: Set(None), + reply_to_id: Set(payload.reply_to_id), + } +} + +pub fn update_request_to_am( + model: message::Model, + payload: UpdateMessageRequest, +) -> message::ActiveModel { + message::ActiveModel { + id: Set(model.id), + channel_id: Set(model.channel_id), + user_id: Set(model.user_id), + content: Set(payload.content), + created_at: Set(model.created_at), + updated_at: Set(Some(Utc::now())), + reply_to_id: Set(model.reply_to_id), + } } diff --git a/src/routes/message/routes.rs b/src/routes/message/routes.rs index f5833d0..d23bac4 100644 --- a/src/routes/message/routes.rs +++ b/src/routes/message/routes.rs @@ -1,8 +1,8 @@ -use axum::{Router, routing::get}; - use super::handlers; +use crate::core::state::AppState; +use axum::{routing::get, Router}; -pub fn router() -> Router { +pub fn router() -> Router { Router::new() .route("/messages", get(handlers::get_all).post(handlers::create)) .route( diff --git a/src/routes/message/service.rs b/src/routes/message/service.rs index da3ee70..4cde781 100644 --- a/src/routes/message/service.rs +++ b/src/routes/message/service.rs @@ -1,21 +1 @@ -use super::domain::Message; - -pub async fn find_all() -> Vec { - todo!() -} - -pub async fn find_by_id(_id: u64) -> Option { - todo!() -} - -pub async fn create(_item: Message) -> Message { - todo!() -} - -pub async fn update(_id: u64, _item: Message) -> Option { - todo!() -} - -pub async fn delete(_id: u64) -> bool { - todo!() -} +// Service layer for Message - currently unused in the simplified architecture diff --git a/src/routes/mod.rs b/src/routes/mod.rs index fda49ca..ff85c63 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -12,12 +12,13 @@ pub mod server; pub mod user; pub fn router() -> OxRouter { - Router::new().merge(auth::routes::router()) - // .merge(user::routes::router()) - // .merge(server::routes::router()) - // .merge(channel::routes::router()) - // .merge(message::routes::router()) - // .merge(group::routes::router()) - // .merge(category::routes::router()) + Router::new() + .merge(auth::routes::router()) + .merge(server::routes::router()) + .merge(category::routes::router()) + .merge(channel::routes::router()) + .merge(group::routes::router()) + .merge(message::routes::router()) + .merge(user::routes::router()) // .merge(attachment::routes::router()) } diff --git a/src/routes/server/dto.rs b/src/routes/server/dto.rs index bfe1999..571001c 100644 --- a/src/routes/server/dto.rs +++ b/src/routes/server/dto.rs @@ -1,10 +1,35 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; -#[derive(Debug, Serialize, Deserialize)] -pub struct CreateServerRequest {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct CreateServerRequest { + #[schema(example = "Mon Super Serveur")] + pub name: String, + pub password: Option, + #[serde(default)] + pub is_default: bool, +} -#[derive(Debug, Serialize, Deserialize)] -pub struct UpdateServerRequest {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct UpdateServerRequest { + pub name: String, + pub password: Option, + pub is_default: bool, + pub default_server_permissions: i64, + pub default_channel_permissions: i64, + pub default_voice_permissions: i64, +} -#[derive(Debug, Serialize, Deserialize)] -pub struct ServerResponse {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct ServerResponse { + pub id: Uuid, + pub name: String, + pub is_default: bool, + pub created_at: DateTime, + pub updated_at: DateTime, + pub default_server_permissions: i64, + pub default_channel_permissions: i64, + pub default_voice_permissions: i64, +} diff --git a/src/routes/server/handlers.rs b/src/routes/server/handlers.rs index dfb714a..3261cbf 100644 --- a/src/routes/server/handlers.rs +++ b/src/routes/server/handlers.rs @@ -1,22 +1,146 @@ -use axum::http::StatusCode; -use axum::response::IntoResponse; +use crate::core::state::AppState; +use crate::http::context::Superuser; +use crate::http::error::HTTPError; +use crate::routes::server::dto::{CreateServerRequest, ServerResponse, UpdateServerRequest}; +use crate::routes::server::mapper; +use axum::{ + extract::{Path, State}, + http::StatusCode, + Json, +}; +use uuid::Uuid; -pub async fn get_all() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Liste tous les serveurs +#[utoipa::path( + get, + path = "/servers", + responses( + (status = 200, description = "Liste des serveurs récupérée avec succès", body = [ServerResponse]), + (status = 500, description = "Erreur interne du serveur") + ), + tag = "Servers" +)] +pub async fn get_all( + State(state): State, +) -> Result>, HTTPError> { + let servers = state.repositories.server.get_all().await?; + Ok(Json( + servers + .into_iter() + .map(mapper::server_model_to_server_response) + .collect(), + )) } -pub async fn get_by_id() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Récupère un serveur par son ID +#[utoipa::path( + get, + path = "/servers/{id}", + responses( + (status = 200, description = "Serveur trouvé", body = ServerResponse), + (status = 404, description = "Serveur non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID du serveur") + ), + tag = "Servers" +)] +pub async fn get_by_id( + State(state): State, + Path(id): Path, +) -> Result, HTTPError> { + let server = state + .repositories + .server + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + Ok(Json(mapper::server_model_to_server_response(server))) } -pub async fn create() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Crée un nouveau serveur +#[utoipa::path( + post, + path = "/servers", + request_body = CreateServerRequest, + responses( + (status = 201, description = "Serveur créé avec succès", body = ServerResponse), + (status = 500, description = "Erreur interne du serveur") + ), + tag = "Servers" +)] +pub async fn create( + _admin: Superuser, + State(state): State, + Json(payload): Json, +) -> Result<(StatusCode, Json), HTTPError> { + let active_model = mapper::create_request_to_am(payload); + let server = state.repositories.server.create(active_model).await?; + Ok(( + StatusCode::CREATED, + Json(mapper::server_model_to_server_response(server)), + )) } -pub async fn update() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Met à jour un serveur existant +#[utoipa::path( + put, + path = "/servers/{id}", + request_body = UpdateServerRequest, + responses( + (status = 200, description = "Serveur mis à jour avec succès", body = ServerResponse), + (status = 404, description = "Serveur non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID du serveur") + ), + tag = "Servers" +)] +pub async fn update( + _admin: Superuser, + State(state): State, + Path(id): Path, + Json(payload): Json, +) -> Result, HTTPError> { + // Vérifier l'existence + let server = state + .repositories + .server + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + let active_model = mapper::update_request_to_am(server.id, payload); + let server = state.repositories.server.update(active_model).await?; + + Ok(Json(mapper::server_model_to_server_response(server))) } -pub async fn delete() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Supprime un serveur +#[utoipa::path( + delete, + path = "/servers/{id}", + responses( + (status = 204, description = "Serveur supprimé avec succès"), + (status = 404, description = "Serveur non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID du serveur") + ), + tag = "Servers" +)] +pub async fn delete( + _admin: Superuser, + State(state): State, + Path(id): Path, +) -> Result { + if state.repositories.server.delete(id).await? { + Ok(StatusCode::NO_CONTENT) + } else { + Err(HTTPError::NotFound) + } } diff --git a/src/routes/server/mapper.rs b/src/routes/server/mapper.rs index 52a3ce5..8ae8c70 100644 --- a/src/routes/server/mapper.rs +++ b/src/routes/server/mapper.rs @@ -1,5 +1,40 @@ -use super::{domain::Server, dto::ServerResponse}; +use crate::models::server; +use crate::routes::server::dto::{CreateServerRequest, ServerResponse, UpdateServerRequest}; +use sea_orm::Set; +use uuid::Uuid; -pub fn to_response(_item: Server) -> ServerResponse { - todo!() +pub fn server_model_to_server_response(model: server::Model) -> ServerResponse { + ServerResponse { + id: model.id, + name: model.name, + is_default: model.is_default, + created_at: model.created_at, + updated_at: model.updated_at, + default_server_permissions: model.default_server_permissions, + default_channel_permissions: model.default_channel_permissions, + default_voice_permissions: model.default_voice_permissions, + } +} + +pub fn create_request_to_am(req: CreateServerRequest) -> server::ActiveModel { + server::ActiveModel { + id: Set(Uuid::new_v4()), + name: Set(req.name), + password: Set(req.password), + is_default: Set(req.is_default), + ..Default::default() + } +} + +pub fn update_request_to_am(id: Uuid, req: UpdateServerRequest) -> server::ActiveModel { + server::ActiveModel { + id: Set(id), + name: Set(req.name), + password: Set(req.password), + is_default: Set(req.is_default), + default_server_permissions: Set(req.default_server_permissions), + default_channel_permissions: Set(req.default_channel_permissions), + default_voice_permissions: Set(req.default_voice_permissions), + ..Default::default() + } } diff --git a/src/routes/server/routes.rs b/src/routes/server/routes.rs index 9d51ca9..fbfa7df 100644 --- a/src/routes/server/routes.rs +++ b/src/routes/server/routes.rs @@ -1,8 +1,8 @@ -use axum::{Router, routing::get}; - use super::handlers; +use crate::core::state::AppState; +use axum::{routing::get, Router}; -pub fn router() -> Router { +pub fn router() -> Router { Router::new() .route("/servers", get(handlers::get_all).post(handlers::create)) .route( diff --git a/src/routes/server/service.rs b/src/routes/server/service.rs index 9686fc8..cc5e02f 100644 --- a/src/routes/server/service.rs +++ b/src/routes/server/service.rs @@ -1,21 +1,2 @@ -use super::domain::Server; - -pub async fn find_all() -> Vec { - todo!() -} - -pub async fn find_by_id(_id: u64) -> Option { - todo!() -} - -pub async fn create(_item: Server) -> Server { - todo!() -} - -pub async fn update(_id: u64, _item: Server) -> Option { - todo!() -} - -pub async fn delete(_id: u64) -> bool { - todo!() -} +// Ce fichier est conservé pour structure mais n'est plus utilisé. +// La logique a été déplacée directement dans les handlers. diff --git a/src/routes/user/domain.rs b/src/routes/user/domain.rs index e6ad9f0..50d5ab6 100644 --- a/src/routes/user/domain.rs +++ b/src/routes/user/domain.rs @@ -1 +1 @@ -pub struct User {} +// Ce fichier est conservé pour la structure. diff --git a/src/routes/user/dto.rs b/src/routes/user/dto.rs index 9772cee..8d0adce 100644 --- a/src/routes/user/dto.rs +++ b/src/routes/user/dto.rs @@ -1,10 +1,29 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; -#[derive(Debug, Serialize, Deserialize)] -pub struct CreateUserRequest {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct CreateUserRequest { + pub username: String, + pub password: String, + pub pub_key: String, + pub is_superuser: bool, +} -#[derive(Debug, Serialize, Deserialize)] -pub struct UpdateUserRequest {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct UpdateUserRequest { + pub username: String, + pub pub_key: String, + pub is_superuser: bool, +} -#[derive(Debug, Serialize, Deserialize)] -pub struct UserResponse {} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct UserResponse { + pub id: Uuid, + pub username: String, + pub pub_key: String, + pub is_superuser: bool, + pub created_at: DateTime, + pub updated_at: DateTime, +} diff --git a/src/routes/user/handlers.rs b/src/routes/user/handlers.rs index dfb714a..3414a13 100644 --- a/src/routes/user/handlers.rs +++ b/src/routes/user/handlers.rs @@ -1,22 +1,182 @@ -use axum::http::StatusCode; -use axum::response::IntoResponse; +use crate::core::state::AppState; +use crate::http::context::Superuser; +use crate::http::error::HTTPError; +use crate::routes::user::dto::{CreateUserRequest, UpdateUserRequest, UserResponse}; +use crate::routes::user::mapper; +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, +}; +use uuid::Uuid; -pub async fn get_all() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Liste tous les utilisateurs +#[utoipa::path( + get, + path = "/users", + responses( + (status = 200, description = "Liste des utilisateurs récupérée avec succès", body = [UserResponse]), + (status = 401, description = "Non autorisé"), + (status = 403, description = "Interdit"), + (status = 500, description = "Erreur interne du serveur") + ), + tag = "Users" +)] +pub async fn get_all( + _admin: Superuser, + State(state): State, +) -> Result>, HTTPError> { + let users = state.repositories.user.get_all().await?; + Ok(Json( + users + .into_iter() + .map(mapper::user_model_to_user_response) + .collect(), + )) } -pub async fn get_by_id() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Récupère un utilisateur par son ID +#[utoipa::path( + get, + path = "/users/{id}", + responses( + (status = 200, description = "Utilisateur trouvé", body = UserResponse), + (status = 401, description = "Non autorisé"), + (status = 403, description = "Interdit"), + (status = 404, description = "Utilisateur non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID de l'utilisateur") + ), + tag = "Users" +)] +pub async fn get_by_id( + _admin: Superuser, + State(state): State, + Path(id): Path, +) -> Result, HTTPError> { + let user = state + .repositories + .user + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + Ok(Json(mapper::user_model_to_user_response(user))) } -pub async fn create() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Crée un nouvel utilisateur +#[utoipa::path( + post, + path = "/users", + request_body = CreateUserRequest, + responses( + (status = 201, description = "Utilisateur créé avec succès", body = UserResponse), + (status = 400, description = "Requête invalide"), + (status = 401, description = "Non autorisé"), + (status = 403, description = "Interdit"), + (status = 500, description = "Erreur interne du serveur") + ), + tag = "Users" +)] +pub async fn create( + _admin: Superuser, + State(state): State, + Json(payload): Json, +) -> Result<(StatusCode, Json), HTTPError> { + // Vérifier si le nom d'utilisateur existe déjà + if state + .repositories + .user + .username_exists(&payload.username) + .await? + { + return Err(HTTPError::BadRequest("Username already exists".to_string())); + } + + let active_model = mapper::create_request_to_am(payload) + .map_err(|e| HTTPError::InternalServerError(e.to_string()))?; + + let user = state.repositories.user.create(active_model).await?; + Ok(( + StatusCode::CREATED, + Json(mapper::user_model_to_user_response(user)), + )) } -pub async fn update() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Met à jour un utilisateur existant +#[utoipa::path( + put, + path = "/users/{id}", + request_body = UpdateUserRequest, + responses( + (status = 200, description = "Utilisateur mis à jour avec succès", body = UserResponse), + (status = 401, description = "Non autorisé"), + (status = 403, description = "Interdit"), + (status = 404, description = "Utilisateur non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID de l'utilisateur") + ), + tag = "Users" +)] +pub async fn update( + _admin: Superuser, + State(state): State, + Path(id): Path, + Json(payload): Json, +) -> Result, HTTPError> { + // Vérifier l'existence + let user = state + .repositories + .user + .get_by_id(id) + .await? + .ok_or(HTTPError::NotFound)?; + + // On pourrait aussi vérifier si le nouveau username existe déjà s'il a changé + if payload.username != user.username + && state + .repositories + .user + .username_exists(&payload.username) + .await? + { + return Err(HTTPError::BadRequest("Username already exists".to_string())); + } + + let active_model = mapper::update_request_to_am(user.id, payload); + let user = state.repositories.user.update(active_model).await?; + + Ok(Json(mapper::user_model_to_user_response(user))) } -pub async fn delete() -> impl IntoResponse { - StatusCode::NOT_IMPLEMENTED +/// Supprime un utilisateur +#[utoipa::path( + delete, + path = "/users/{id}", + responses( + (status = 204, description = "Utilisateur supprimé avec succès"), + (status = 401, description = "Non autorisé"), + (status = 403, description = "Interdit"), + (status = 404, description = "Utilisateur non trouvé"), + (status = 500, description = "Erreur interne du serveur") + ), + params( + ("id" = Uuid, Path, description = "ID de l'utilisateur") + ), + tag = "Users" +)] +pub async fn delete( + _admin: Superuser, + State(state): State, + Path(id): Path, +) -> Result { + if state.repositories.user.delete(id).await? { + Ok(StatusCode::NO_CONTENT) + } else { + Err(HTTPError::NotFound) + } } diff --git a/src/routes/user/mapper.rs b/src/routes/user/mapper.rs index 62c6467..f5b276d 100644 --- a/src/routes/user/mapper.rs +++ b/src/routes/user/mapper.rs @@ -1,5 +1,41 @@ -use super::{domain::User, dto::UserResponse}; +use crate::auth::password::hash_password; +use crate::models::user; +use crate::routes::user::dto::{CreateUserRequest, UpdateUserRequest, UserResponse}; +use anyhow::Result as AnyResult; +use sea_orm::{NotSet, Set}; +use uuid::Uuid; -pub fn to_response(_user: User) -> UserResponse { - todo!() +pub fn user_model_to_user_response(model: user::Model) -> UserResponse { + UserResponse { + id: model.id, + username: model.username, + pub_key: model.pub_key, + is_superuser: model.is_superuser, + created_at: model.created_at, + updated_at: model.updated_at, + } +} + +pub fn create_request_to_am(payload: CreateUserRequest) -> AnyResult { + Ok(user::ActiveModel { + id: Set(Uuid::new_v4()), + username: Set(payload.username), + password: Set(hash_password(&payload.password)?), + pub_key: Set(payload.pub_key), + is_superuser: Set(payload.is_superuser), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + }) +} + +pub fn update_request_to_am(id: Uuid, payload: UpdateUserRequest) -> user::ActiveModel { + user::ActiveModel { + id: Set(id), + username: Set(payload.username), + password: NotSet, // On ne change pas le password via PUT général + pub_key: Set(payload.pub_key), + is_superuser: Set(payload.is_superuser), + updated_at: Set(chrono::Utc::now()), + ..Default::default() + } } diff --git a/src/routes/user/routes.rs b/src/routes/user/routes.rs index 68bb761..bc1f90d 100644 --- a/src/routes/user/routes.rs +++ b/src/routes/user/routes.rs @@ -1,8 +1,9 @@ +use crate::core::state::AppState; use axum::{routing::get, Router}; use super::handlers; -pub fn router() -> Router { +pub fn router() -> Router { Router::new() .route("/users", get(handlers::get_all).post(handlers::create)) .route( diff --git a/src/routes/user/service.rs b/src/routes/user/service.rs index 8ac5420..860cac4 100644 --- a/src/routes/user/service.rs +++ b/src/routes/user/service.rs @@ -1,21 +1 @@ -use super::domain::User; - -pub async fn find_all() -> Vec { - todo!() -} - -pub async fn find_by_id(_id: u64) -> Option { - todo!() -} - -pub async fn create(_user: User) -> User { - todo!() -} - -pub async fn update(_id: u64, _user: User) -> Option { - todo!() -} - -pub async fn delete(_id: u64) -> bool { - todo!() -} +// Ce fichier est conservé pour la structure mais la logique est directement dans les handlers. diff --git a/src/udp/metrics.rs b/src/udp/metrics.rs index 504f6f0..0fbb18f 100644 --- a/src/udp/metrics.rs +++ b/src/udp/metrics.rs @@ -27,6 +27,8 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; +use crate::metrics::{Metrics, MetricsSnapshot}; + // ── Compteurs ──────────────────────────────────────────────────────────────── /// Compteurs atomiques du serveur UDP. @@ -94,6 +96,7 @@ impl UdpMetrics { /// Prend un instantané cohérent de tous les compteurs. pub fn snapshot(&self) -> UdpMetricsSnapshot { UdpMetricsSnapshot { + taken_at: Instant::now(), packets_received: self.packets_received.load(Ordering::Relaxed), bytes_received: self.bytes_received.load(Ordering::Relaxed), packets_sent: self.packets_sent.load(Ordering::Relaxed), @@ -105,14 +108,23 @@ impl UdpMetrics { } } +impl Metrics for UdpMetrics { + type Snapshot = UdpMetricsSnapshot; + + fn snapshot(&self) -> UdpMetricsSnapshot { + self.snapshot() + } +} + // ── Snapshot ───────────────────────────────────────────────────────────────── /// Lecture cohérente de l'ensemble des compteurs à un instant T. /// /// Permet de calculer des deltas et des taux entre deux points dans le temps /// sans bloquer la boucle de routage. -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct UdpMetricsSnapshot { + pub taken_at: Instant, pub packets_received: u64, pub bytes_received: u64, pub packets_sent: u64, @@ -124,11 +136,12 @@ pub struct UdpMetricsSnapshot { impl UdpMetricsSnapshot { /// Calcule les taux moyens par seconde depuis un snapshot précédent. - /// - /// `elapsed` est la durée réelle écoulée entre les deux snapshots. - pub fn rates_since(&self, previous: &Self, elapsed: Duration) -> UdpRates { - // On évite la division par zéro si l'interval est infinitésimal. - let secs = elapsed.as_secs_f64().max(f64::EPSILON); + pub fn rates_since(&self, previous: &Self) -> UdpRates { + let secs = self + .taken_at + .duration_since(previous.taken_at) + .as_secs_f64() + .max(f64::EPSILON); UdpRates { packets_received_per_sec: self @@ -151,6 +164,12 @@ impl UdpMetricsSnapshot { } } +impl MetricsSnapshot for UdpMetricsSnapshot { + fn taken_at(&self) -> Instant { + self.taken_at + } +} + // ── Taux ───────────────────────────────────────────────────────────────────── /// Taux moyens par seconde calculés entre deux [`UdpMetricsSnapshot`]. @@ -194,15 +213,12 @@ pub fn spawn_reporter(metrics: Arc, interval: Duration) { ticker.tick().await; let mut prev_snapshot = metrics.snapshot(); - let mut prev_instant = Instant::now(); loop { ticker.tick().await; - let now = Instant::now(); let current = metrics.snapshot(); - let elapsed = now.duration_since(prev_instant); - let rates = current.rates_since(&prev_snapshot, elapsed); + let rates = current.rates_since(&prev_snapshot); tracing::info!( // ── Cumulatifs ── @@ -219,12 +235,10 @@ pub fn spawn_reporter(metrics: Arc, interval: Duration) { pkts_tx_s = format!("{:.1}", rates.packets_sent_per_sec), bytes_tx_s = format!("{:.0}", rates.bytes_sent_per_sec), pkts_dropped_s = format!("{:.1}", rates.packets_dropped_per_sec), - interval_ms = elapsed.as_millis(), "UDP metrics" ); prev_snapshot = current; - prev_instant = now; } }); } diff --git a/src/udp/server.rs b/src/udp/server.rs index fade881..b89d39f 100644 --- a/src/udp/server.rs +++ b/src/udp/server.rs @@ -69,9 +69,8 @@ impl UdpServer { /// /// Retourne le serveur et un [`broadcast::Sender`] pour déclencher le /// shutdown gracieux. - pub fn new(network: &NetworkConfig) -> (Self, broadcast::Sender<()>) { + pub fn new(network: &NetworkConfig, metrics: Arc) -> (Self, broadcast::Sender<()>) { let bind_addr = SocketAddr::new(network.host.into(), network.udp_port); - let metrics = UdpMetrics::new(); let (shutdown_tx, shutdown_rx) = broadcast::channel(1); ( Self {