From 260671f3ad0ca231136855dc4b7ef1d65b61d99b Mon Sep 17 00:00:00 2001 From: Nell Date: Sat, 15 Nov 2025 00:34:04 +0100 Subject: [PATCH] init --- Cargo.lock | 12 +++ Cargo.toml | 3 +- src/app/app.rs | 55 ++++++++++-- src/app/http/api/channel.rs | 42 ++++++++++ src/app/http/api/core.rs | 3 + src/app/http/api/message.rs | 22 ++++- src/app/http/api/mod.rs | 3 +- src/app/http/api/server.rs | 36 ++++++++ src/app/http/middleware/auth_middleware.rs | 97 ++++++++++++++++++++++ src/app/http/middleware/mod.rs | 1 + src/app/http/mod.rs | 2 + src/app/http/request_context.rs | 40 +++++++++ src/app/http/router.rs | 28 +++++-- src/app/http/ws/ws.rs | 0 src/db/context.rs | 1 + src/db/mod.rs | 2 +- src/network/http/server.rs | 3 +- src/network/udp/server.rs | 13 ++- 18 files changed, 338 insertions(+), 25 deletions(-) create mode 100644 src/app/http/api/channel.rs create mode 100644 src/app/http/middleware/auth_middleware.rs create mode 100644 src/app/http/middleware/mod.rs create mode 100644 src/app/http/request_context.rs create mode 100644 src/app/http/ws/ws.rs diff --git a/Cargo.lock b/Cargo.lock index 9b79936..e17a3b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,17 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atoi" version = "2.0.0" @@ -1131,6 +1142,7 @@ name = "ox_speak_server" version = "0.1.0" dependencies = [ "arc-swap", + "async-trait", "axum", "chrono", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 9dea515..dc95ef1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,4 +31,5 @@ hyper = "1.7" sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "sqlite", "postgres", "mysql", "uuid", "chrono", "migrate"] } uuid = { version = "1.18", features = ["v4", "v7", "serde"] } -chrono = { version = "0.4", features = ["serde"] } \ No newline at end of file +chrono = { version = "0.4", features = ["serde"] } +async-trait = "0.1.89" \ No newline at end of file diff --git a/src/app/app.rs b/src/app/app.rs index db7bedd..f6c3a28 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -2,11 +2,19 @@ use std::io; use std::sync::Arc; use std::net::SocketAddr; use crate::app::http::router::configure_routes; +use crate::db::context::DbContext; use crate::network::http::server::HttpServer; use crate::network::udp::server::UdpServer; use crate::utils::config::Config; use crate::utils::logger::ContextLogger; +#[derive(Clone)] +pub struct Context { + udp_server: Arc, + http_server: Arc, + db: Arc +} + pub struct App { config: Config, context: Arc, @@ -33,9 +41,17 @@ impl App { } }; + let db = match DbContext::new("sqlite:ox_speak_server.db").await { + Ok(db) => db, + Err(e) => { + return Err(io::Error::new(io::ErrorKind::Other, format!("Failed to create database context: {}", e))) + } + }; + let context = Context { - udp_server, - http_server, + udp_server: Arc::new(udp_server), + http_server: Arc::new(http_server), + db: Arc::new(db) }; Ok(Self { @@ -47,15 +63,38 @@ impl App { pub async fn run(self) -> io::Result<()> { self.logger.info("Starting application"); - self.context.udp_server.run().await?; - self.context.http_server.run().await?; + let udp_server = self.context.udp_server.clone(); + let http_server = self.context.http_server.clone(); + let db = self.context.db.clone(); + + let udp_handle = tokio::spawn(async move { + udp_server.run().await + }); + + let router = configure_routes(db); + let http_handle = tokio::spawn(async move { + http_server.run(router).await + }); + + tokio::select! { + result = udp_handle => { + match result { + Ok(Ok(())) => println!("UDP server terminated"), + Ok(Err(e)) => return Err(e), + Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), + } + } + result = http_handle => { + match result { + Ok(Ok(())) => println!("HTTP server terminated"), + Ok(Err(e)) => return Err(e), + Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), + } + } + } Ok(()) } } -struct Context { - udp_server: UdpServer, - http_server: HttpServer -} \ No newline at end of file diff --git a/src/app/http/api/channel.rs b/src/app/http/api/channel.rs new file mode 100644 index 0000000..85ba599 --- /dev/null +++ b/src/app/http/api/channel.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; +use axum::{Extension, Json, Router}; +use axum::body::Body; +use axum::extract::{Path, State}; +use axum::http::Request; +use axum::response::IntoResponse; +use axum::routing::{get, post}; +use hyper::StatusCode; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use crate::app::http::request_context::RequestContext; +use crate::app::http::router::AppState; +use crate::db::{Message, Server}; + +pub fn routes() -> Router> { + Router::new() + .route("/:server_id/", get(channel_list)) + .route("/create/", post(channel_create)) +} + +#[derive(Debug, Deserialize)] +pub struct CreateServerRequest { + name: String, +} + +async fn channel_list( + State(state): State>, + Extension(ctx): Extension, + Path(server_id): Path +) -> Result>, StatusCode> { + Ok(Json(vec![])) +} + +async fn channel_create( + State(state): State>, + Extension(ctx): Extension, + Path(server_id): Path, + Json(payload): Json, +) -> Result, StatusCode> { + let server = Server::new("test".to_string(), None); + Ok(Json(server)) +} \ No newline at end of file diff --git a/src/app/http/api/core.rs b/src/app/http/api/core.rs index 3303277..146d702 100644 --- a/src/app/http/api/core.rs +++ b/src/app/http/api/core.rs @@ -1,7 +1,10 @@ +use std::sync::Arc; use axum::http::StatusCode; use axum::{Json, Router}; +use axum::extract::State; use axum::routing::get; use serde_json::json; +use crate::app::http::router::AppState; pub fn routes() -> Router { Router::new() diff --git a/src/app/http/api/message.rs b/src/app/http/api/message.rs index 12e96ef..656ef9a 100644 --- a/src/app/http/api/message.rs +++ b/src/app/http/api/message.rs @@ -1,6 +1,22 @@ -use axum::Router; +use std::sync::Arc; +use axum::extract::{Path, State}; +use axum::{Json, Router}; +use axum::routing::get; +use axum::http::StatusCode; +use crate::app::http::router::AppState; +use crate::db::Message; -pub fn routes() -> Router { +pub fn routes() -> Router> { Router::new() - .nest("/message", Router::new()) + .route("/message/:channel_id/", get(list_messages)) +} + +async fn list_messages( + State(state): State>, + Path(channel_id): Path +) -> Result>, StatusCode> { + // todo : Récupérer les messages du channel + // vérifier les permissions + + Ok(Json(vec![])) } \ No newline at end of file diff --git a/src/app/http/api/mod.rs b/src/app/http/api/mod.rs index acfc57a..8e06e51 100644 --- a/src/app/http/api/mod.rs +++ b/src/app/http/api/mod.rs @@ -1,3 +1,4 @@ pub mod core; +pub mod server; pub mod message; -pub mod server; \ No newline at end of file +pub mod channel; \ No newline at end of file diff --git a/src/app/http/api/server.rs b/src/app/http/api/server.rs index e69de29..e969c36 100644 --- a/src/app/http/api/server.rs +++ b/src/app/http/api/server.rs @@ -0,0 +1,36 @@ +use std::sync::Arc; +use axum::{Extension, Json, Router}; +use axum::extract::State; +use axum::response::IntoResponse; +use axum::routing::get; +use hyper::StatusCode; +use serde::{Deserialize, Serialize}; +use crate::app::http::request_context::RequestContext; +use crate::app::http::router::AppState; +use crate::db::{Message, Server}; + +pub fn routes() -> Router> { + Router::new() + .route("/", get(list_servers)) + .route("/create", get(create_server)) +} + +#[derive(Debug, Deserialize)] +pub struct CreateServerRequest { + name: String, +} + +async fn list_servers( + State(state): State>, + Extension(ctx): Extension, +) -> Result>, StatusCode> { + Ok(Json(vec![])) +} + +async fn create_server( + State(state): State>, + Json(payload): Json +) -> Result, StatusCode> { + let server = Server::new("test".to_string(), None); + Ok(Json(server)) +} \ No newline at end of file diff --git a/src/app/http/middleware/auth_middleware.rs b/src/app/http/middleware/auth_middleware.rs new file mode 100644 index 0000000..7bd72b3 --- /dev/null +++ b/src/app/http/middleware/auth_middleware.rs @@ -0,0 +1,97 @@ +use axum::{ + body::Body, + extract::{Request, State}, + http::header, + middleware::Next, + response::Response, +}; +use std::collections::HashMap; +use std::sync::Arc; +use crate::app::http::request_context::RequestContext; +use crate::db::context::DbContext; + +/// Middleware d'authentification. +pub async fn auth_middleware( + State(db): State>, + req: Request, + next: Next, +) -> Response { + // Extraire les informations de base + let method = req.method().clone(); + let uri = req.uri().clone(); + let path = uri.path().to_string(); + let query_string = uri.query().map(|q| q.to_string()); + let headers = req.headers().clone(); + + // Extraire l'IP du client + let ip_address = headers + .get("x-forwarded-for") + .or_else(|| headers.get("x-real-ip")) + .and_then(|h| h.to_str().ok()) + .map(|s| s.split(',').next().unwrap_or(s).trim().to_string()) + .or_else(|| { + // Fallback sur l'adresse de connexion si disponible + req.extensions() + .get::() + .map(|addr| addr.ip().to_string()) + }); + + // Extraire User-Agent + let user_agent = headers + .get(header::USER_AGENT) + .and_then(|h| h.to_str().ok()) + .map(|s| s.to_string()); + + // Extraire Content-Type + let content_type = headers + .get(header::CONTENT_TYPE) + .and_then(|h| h.to_str().ok()) + .map(|s| s.to_string()); + + // Session simple (vous pouvez l'enrichir avec un vrai système de session) + let session = Arc::new(HashMap::new()); + + // Extraire le token d'authentification + let mut auth_token = None; + let mut user = None; + + if let Some(auth_header) = headers.get(header::AUTHORIZATION) { + if let Ok(auth_str) = auth_header.to_str() { + if let Some(token) = auth_str.strip_prefix("Bearer ") { + auth_token = Some(token.to_string()); + + // TODO: Récupérer l'utilisateur depuis la DB + // match db.repositories().user_repository.find_by_token(token).await { + // Ok(found_user) => user = Some(found_user), + // Err(e) => log::warn!("Token invalide: {}", e), + // } + + log::debug!("Token d'authentification détecté"); + } + } + } + + // Construire le contexte complet + let context = RequestContext { + user, + method, + uri, + path, + query_string, + headers, + session, + ip_address, + user_agent, + content_type, + auth_token, + }; + + log::debug!("Request: {}", context.debug_info()); + + // Injecter le contexte dans les extensions + let (mut parts, body) = req.into_parts(); + parts.extensions.insert(context); + let req = Request::from_parts(parts, body); + + next.run(req).await +} \ No newline at end of file diff --git a/src/app/http/middleware/mod.rs b/src/app/http/middleware/mod.rs new file mode 100644 index 0000000..e12d527 --- /dev/null +++ b/src/app/http/middleware/mod.rs @@ -0,0 +1 @@ +pub mod auth_middleware; \ No newline at end of file diff --git a/src/app/http/mod.rs b/src/app/http/mod.rs index c0cce76..9a90b00 100644 --- a/src/app/http/mod.rs +++ b/src/app/http/mod.rs @@ -1,3 +1,5 @@ pub mod router; pub mod api; +mod request_context; +mod middleware; diff --git a/src/app/http/request_context.rs b/src/app/http/request_context.rs new file mode 100644 index 0000000..d254bc2 --- /dev/null +++ b/src/app/http/request_context.rs @@ -0,0 +1,40 @@ +use axum::http::{header::HeaderMap, Method, Uri}; +use std::collections::HashMap; +use std::sync::Arc; +use crate::db::models::User; + +/// Contexte de requête (style Django) +#[derive(Clone)] +pub struct RequestContext { + pub user: Option, + pub method: Method, + pub uri: Uri, + pub path: String, + pub query_string: Option, + pub headers: HeaderMap, + pub session: Arc>, + pub ip_address: Option, + pub user_agent: Option, + pub content_type: Option, + pub auth_token: Option, +} + +impl RequestContext { + pub fn is_authenticated(&self) -> bool { + self.user.is_some() + } + + pub fn get_header(&self, name: &str) -> Option<&str> { + self.headers.get(name)?.to_str().ok() + } + + pub fn debug_info(&self) -> String { + format!( + "{} {} - User: {} - IP: {}", + self.method, + self.path, + self.user.as_ref().map(|u| u.username.as_str()).unwrap_or("Anonymous"), + self.ip_address.as_deref().unwrap_or("unknown"), + ) + } +} \ No newline at end of file diff --git a/src/app/http/router.rs b/src/app/http/router.rs index 8a55e5c..6342cce 100644 --- a/src/app/http/router.rs +++ b/src/app/http/router.rs @@ -1,13 +1,29 @@ -use axum::Router; +use std::sync::Arc; +use axum::{Router, middleware}; use crate::app::http::api; -use crate::app::http::api::core; +use crate::app::http::api::{channel, core, message, server}; +use crate::app::http::middleware::auth_middleware; +use crate::app::http::middleware::auth_middleware::auth_middleware; +use crate::db::context::DbContext; -pub fn configure_routes() -> Router { - let api_router = Router::new(); +#[derive(Clone)] +pub struct AppState { + db: Arc +} +pub fn configure_routes(db_context: Arc) -> Router { + let app_state = Arc::new(AppState { + db: db_context.clone() + }); + + let api_router = Router::new() + .nest("/server/", server::routes()) + .nest("/channel/", channel::routes()) + .nest("/message/", message::routes()) + .with_state(app_state); Router::new() .merge(core::routes()) - .nest("/api", api_router ) - + .nest("/api", api_router) + .layer(middleware::from_fn_with_state(db_context.clone(), auth_middleware)) } diff --git a/src/app/http/ws/ws.rs b/src/app/http/ws/ws.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/db/context.rs b/src/db/context.rs index dec4d0e..b443e8c 100644 --- a/src/db/context.rs +++ b/src/db/context.rs @@ -15,6 +15,7 @@ struct Repositories { channel_user: ChannelUserRepository } +#[derive(Clone)] pub struct DbContext { pool: Arc, diff --git a/src/db/mod.rs b/src/db/mod.rs index 776bd28..7e8198c 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,6 +1,6 @@ pub mod models; pub mod repositories; -mod context; +pub mod context; pub use models::*; pub use repositories::*; \ No newline at end of file diff --git a/src/network/http/server.rs b/src/network/http/server.rs index c995084..3bac556 100644 --- a/src/network/http/server.rs +++ b/src/network/http/server.rs @@ -4,6 +4,7 @@ use axum::{Router}; use tokio::net::TcpListener; use crate::utils::logger::ContextLogger; +#[derive(Clone)] pub struct HttpServer { bind_addr: SocketAddr, logger: ContextLogger, @@ -20,7 +21,7 @@ impl HttpServer { Ok(Self { bind_addr, logger }) } - pub async fn run(self, router: Router) -> io::Result<()> { + pub async fn run(&self, router: Router) -> io::Result<()> { self.logger.info(&format!("Starting HTTP server on {}", self.bind_addr)); let listener = TcpListener::bind(self.bind_addr).await?; diff --git a/src/network/udp/server.rs b/src/network/udp/server.rs index 82472d2..0ad8fa8 100644 --- a/src/network/udp/server.rs +++ b/src/network/udp/server.rs @@ -14,9 +14,9 @@ use tokio::task; use crate::utils::shared_store::{SharedArcMap, SharedArcVec}; use crate::utils::toolbox::number_of_cpus; use crate::utils::logger::ContextLogger; -use crate::{log_info, log_warn, log_error, log_debug}; +#[derive(Clone)] pub struct UdpServer { // table de routage channel -> socket table_router: SharedArcMap>, @@ -52,7 +52,12 @@ impl UdpServer { }) } - pub async fn run(self) -> io::Result<()> { + + pub fn router(&self) -> SharedArcMap> { + self.table_router.clone() + } + + pub async fn run(&self) -> io::Result<()> { // S'assure qu'on est sur une runtime (utile si appelé hors #[tokio::main]) let _ = Handle::try_current().map_err(|e| { self.logger.error(&format!("Runtime Tokio not available: {}", e)); @@ -78,7 +83,7 @@ impl UdpServer { // Impl Linux/macOS (SO_REUSEPORT) // ------------------------- #[cfg(unix)] - async fn run_unix(self) -> io::Result<()> { + async fn run_unix(&self) -> io::Result<()> { use socket2::{Domain, Protocol, Socket, Type}; self.logger.info(&format!("Starting UDP server on {} with {} workers - Unix mode", self.bind_addr, self.workers)); @@ -121,7 +126,7 @@ impl UdpServer { // Impl Windows (1 socket partagé, N tâches concurrentes) // ------------------------- #[cfg(windows)] - async fn run_windows(self) -> io::Result<()> { + async fn run_windows(&self) -> io::Result<()> { self.logger.info(&format!("Starting UDP server on {} with {} workers - Win mode", self.bind_addr, self.workers)); let udp = UdpSocket::bind(self.bind_addr).await?;