This commit is contained in:
2025-11-15 00:34:04 +01:00
parent 38ed2842ff
commit 260671f3ad
18 changed files with 338 additions and 25 deletions

12
Cargo.lock generated
View File

@@ -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",

View File

@@ -32,3 +32,4 @@ 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"] }
async-trait = "0.1.89"

View File

@@ -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<UdpServer>,
http_server: Arc<HttpServer>,
db: Arc<DbContext>
}
pub struct App {
config: Config,
context: Arc<Context>,
@@ -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
}

View File

@@ -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<Arc<AppState>> {
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<Arc<AppState>>,
Extension(ctx): Extension<RequestContext>,
Path(server_id): Path<Uuid>
) -> Result<Json<Vec<Server>>, StatusCode> {
Ok(Json(vec![]))
}
async fn channel_create(
State(state): State<Arc<AppState>>,
Extension(ctx): Extension<RequestContext>,
Path(server_id): Path<Uuid>,
Json(payload): Json<CreateServerRequest>,
) -> Result<Json<Server>, StatusCode> {
let server = Server::new("test".to_string(), None);
Ok(Json(server))
}

View File

@@ -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()

View File

@@ -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<Arc<AppState>> {
Router::new()
.nest("/message", Router::new())
.route("/message/:channel_id/", get(list_messages))
}
async fn list_messages(
State(state): State<Arc<AppState>>,
Path(channel_id): Path<i64>
) -> Result<Json<Vec<Message>>, StatusCode> {
// todo : Récupérer les messages du channel
// vérifier les permissions
Ok(Json(vec![]))
}

View File

@@ -1,3 +1,4 @@
pub mod core;
pub mod message;
pub mod server;
pub mod message;
pub mod channel;

View File

@@ -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<Arc<AppState>> {
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<Arc<AppState>>,
Extension(ctx): Extension<RequestContext>,
) -> Result<Json<Vec<Server>>, StatusCode> {
Ok(Json(vec![]))
}
async fn create_server(
State(state): State<Arc<AppState>>,
Json(payload): Json<CreateServerRequest>
) -> Result<Json<Server>, StatusCode> {
let server = Server::new("test".to_string(), None);
Ok(Json(server))
}

View File

@@ -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<Arc<DbContext>>,
req: Request<Body>,
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::<std::net::SocketAddr>()
.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
}

View File

@@ -0,0 +1 @@
pub mod auth_middleware;

View File

@@ -1,3 +1,5 @@
pub mod router;
pub mod api;
mod request_context;
mod middleware;

View File

@@ -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<User>,
pub method: Method,
pub uri: Uri,
pub path: String,
pub query_string: Option<String>,
pub headers: HeaderMap,
pub session: Arc<HashMap<String, String>>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub content_type: Option<String>,
pub auth_token: Option<String>,
}
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"),
)
}
}

View File

@@ -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<DbContext>
}
pub fn configure_routes(db_context: Arc<DbContext>) -> 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))
}

0
src/app/http/ws/ws.rs Normal file
View File

View File

@@ -15,6 +15,7 @@ struct Repositories {
channel_user: ChannelUserRepository
}
#[derive(Clone)]
pub struct DbContext {
pool: Arc<SqlitePool>,

View File

@@ -1,6 +1,6 @@
pub mod models;
pub mod repositories;
mod context;
pub mod context;
pub use models::*;
pub use repositories::*;

View File

@@ -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?;

View File

@@ -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<String, SharedArcVec<SocketAddr>>,
@@ -52,7 +52,12 @@ impl UdpServer {
})
}
pub async fn run(self) -> io::Result<()> {
pub fn router(&self) -> SharedArcMap<String, SharedArcVec<SocketAddr>> {
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?;