Init
This commit is contained in:
59
src/app/app.rs
Normal file
59
src/app/app.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use crate::app::AppState;
|
||||
use crate::config::Config;
|
||||
use crate::database::Database;
|
||||
use crate::network::http::HTTPServer;
|
||||
use crate::network::udp::UDPServer;
|
||||
|
||||
pub struct App {
|
||||
config: Config,
|
||||
|
||||
db: Database,
|
||||
udp_server: UDPServer,
|
||||
http_server: HTTPServer
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub async fn init(config: Config) -> Self {
|
||||
|
||||
let db = Database::init(&config.database_url()).await.expect("Failed to initialize database");
|
||||
|
||||
let state = AppState::new(db.clone());
|
||||
// let state = AppState::new();
|
||||
|
||||
let udp_server = UDPServer::new(config.bind_addr());
|
||||
let http_server = HTTPServer::new(config.bind_addr(), state);
|
||||
|
||||
Self {
|
||||
config,
|
||||
db,
|
||||
udp_server,
|
||||
http_server
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&self) {
|
||||
println!("Application démarrée. Appuyez sur Ctrl+C pour arrêter.");
|
||||
|
||||
// Le select arbitre la course
|
||||
tokio::select! {
|
||||
// Branche 1 : Le serveur tourne. S'il crash ou finit (peu probable), on sort.
|
||||
|
||||
_ = self.udp_server.run() => {
|
||||
println!("Le serveur UDP s'est arrêté de lui-même (erreur ?).");
|
||||
}
|
||||
|
||||
_ = self.http_server.run() => {
|
||||
println!("Le serveur HTTP s'est arrêté de lui-même (erreur ?).");
|
||||
}
|
||||
|
||||
// Branche 2 : On écoute le signal d'arrêt.
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
println!("Signal d'arrêt reçu !");
|
||||
}
|
||||
}
|
||||
|
||||
println!("Nettoyage et fermeture de l'application.");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
5
src/app/mod.rs
Normal file
5
src/app/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod app;
|
||||
mod state;
|
||||
|
||||
pub use app::App;
|
||||
pub use state::AppState;
|
||||
23
src/app/state.rs
Normal file
23
src/app/state.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use crate::database::Database;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub db: Database
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(db: Database) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
}
|
||||
|
||||
// #[derive(Clone)]
|
||||
// pub struct AppState {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// impl AppState {
|
||||
// pub fn new() -> Self {
|
||||
// Self { }
|
||||
// }
|
||||
// }
|
||||
160
src/config/config.rs
Normal file
160
src/config/config.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct Config {
|
||||
pub server: ServerConfig,
|
||||
pub database: DatabaseConfig,
|
||||
pub jwt: JwtConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub bind_addr: String,
|
||||
pub mode: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct DatabaseConfig {
|
||||
#[serde(rename = "type")]
|
||||
pub db_type: String,
|
||||
|
||||
// SQLite
|
||||
pub path: Option<String>,
|
||||
|
||||
// PostgreSQL / MySQL
|
||||
pub host: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub user: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub dbname: Option<String>,
|
||||
|
||||
// PostgreSQL specific
|
||||
pub sslmode: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct JwtConfig {
|
||||
pub secret: String,
|
||||
pub expiration: u64,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Loads configuration from a TOML file
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
|
||||
let content = fs::read_to_string(path)
|
||||
.map_err(|e| ConfigError::IoError(e.to_string()))?;
|
||||
|
||||
let config: Config = toml::from_str(&content)
|
||||
.map_err(|e| ConfigError::ParseError(e.to_string()))?;
|
||||
|
||||
config.validate()?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Validates the configuration
|
||||
fn validate(&self) -> Result<(), ConfigError> {
|
||||
// Validate server mode
|
||||
if self.server.mode != "debug" && self.server.mode != "release" {
|
||||
return Err(ConfigError::ValidationError(
|
||||
format!("Mode invalide: '{}'. Doit être 'debug' ou 'release'", self.server.mode)
|
||||
));
|
||||
}
|
||||
|
||||
// Validate database type
|
||||
match self.database.db_type.as_str() {
|
||||
"sqlite" => {
|
||||
if self.database.path.is_none() {
|
||||
return Err(ConfigError::ValidationError(
|
||||
"SQLite database path is required".to_string()
|
||||
));
|
||||
}
|
||||
}
|
||||
"postgres" | "mysql" => {
|
||||
if self.database.host.is_none()
|
||||
|| self.database.port.is_none()
|
||||
|| self.database.user.is_none()
|
||||
|| self.database.password.is_none()
|
||||
|| self.database.dbname.is_none() {
|
||||
return Err(ConfigError::ValidationError(
|
||||
format!("Incomplete configuration for {}", self.database.db_type)
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(ConfigError::ValidationError(
|
||||
format!("Invalid database type: '{}'. Must be 'sqlite', 'postgres' or 'mysql'",
|
||||
self.database.db_type)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate JWT secret
|
||||
if self.jwt.secret == "your_very_secure_jwt_secret_to_change" {
|
||||
eprintln!("⚠️ WARNING: You are using the default JWT secret! Change it in production!");
|
||||
}
|
||||
|
||||
if self.jwt.secret.len() < 32 {
|
||||
eprintln!("⚠️ WARNING: JWT secret is too short (minimum 32 characters recommended)");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the database connection string
|
||||
pub fn database_url(&self) -> String {
|
||||
match self.database.db_type.as_str() {
|
||||
"sqlite" => {
|
||||
format!("sqlite://{}?mode=rwc", self.database.path.as_ref().unwrap())
|
||||
}
|
||||
"postgres" => {
|
||||
format!(
|
||||
"postgresql://{}:{}@{}:{}/{}?sslmode={}",
|
||||
self.database.user.as_ref().unwrap(),
|
||||
self.database.password.as_ref().unwrap(),
|
||||
self.database.host.as_ref().unwrap(),
|
||||
self.database.port.unwrap(),
|
||||
self.database.dbname.as_ref().unwrap(),
|
||||
self.database.sslmode.as_ref().unwrap_or(&"disable".to_string())
|
||||
)
|
||||
}
|
||||
"mysql" => {
|
||||
format!(
|
||||
"mysql://{}:{}@{}:{}/{}",
|
||||
self.database.user.as_ref().unwrap(),
|
||||
self.database.password.as_ref().unwrap(),
|
||||
self.database.host.as_ref().unwrap(),
|
||||
self.database.port.unwrap(),
|
||||
self.database.dbname.as_ref().unwrap()
|
||||
)
|
||||
}
|
||||
_ => panic!("Unsupported database type")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the SocketAddr to bind to
|
||||
pub fn bind_addr(&self) -> std::net::SocketAddr {
|
||||
self.server.bind_addr.parse().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
IoError(String),
|
||||
ParseError(String),
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
ConfigError::IoError(msg) => write!(f, "IO Error: {}", msg),
|
||||
ConfigError::ParseError(msg) => write!(f, "Parsing Error: {}", msg),
|
||||
ConfigError::ValidationError(msg) => write!(f, "Validation Error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConfigError {}
|
||||
3
src/config/mod.rs
Normal file
3
src/config/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod config;
|
||||
|
||||
pub use config::Config;
|
||||
34
src/database/database.rs
Normal file
34
src/database/database.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::time::Duration;
|
||||
use sea_orm::{ConnectOptions, Database as SeaDatabase, DatabaseConnection, DbErr};
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
pub connection: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub async fn init(dsn: &str) -> Result<Self, DbErr> {
|
||||
let mut opt = ConnectOptions::new(dsn);
|
||||
opt.max_connections(100)
|
||||
.min_connections(5)
|
||||
.connect_timeout(Duration::from_secs(8))
|
||||
.acquire_timeout(Duration::from_secs(8))
|
||||
.sqlx_logging(true)
|
||||
.sqlx_logging_level(log::LevelFilter::Debug);
|
||||
|
||||
let connection = SeaDatabase::connect(opt).await?;
|
||||
|
||||
// On lance les migrations ici.
|
||||
// Si ça échoue, le programme s'arrête proprement à l'init.
|
||||
Migrator::up(&connection, None).await?;
|
||||
|
||||
Ok(Self { connection })
|
||||
}
|
||||
|
||||
// Tu peux ajouter ici tes méthodes helpers si tu veux encapsuler SeaORM
|
||||
// ex: pub async fn find_user(...)
|
||||
pub fn get_connection(&self) -> &DatabaseConnection {
|
||||
&self.connection
|
||||
}
|
||||
}
|
||||
3
src/database/mod.rs
Normal file
3
src/database/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod database;
|
||||
|
||||
pub use database::Database;
|
||||
9
src/lib.rs
Normal file
9
src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod config;
|
||||
pub mod app;
|
||||
pub mod network;
|
||||
pub mod utils;
|
||||
|
||||
pub mod database;
|
||||
pub mod models;
|
||||
pub mod serializers;
|
||||
pub mod repositories;
|
||||
17
src/main.rs
Normal file
17
src/main.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use ox_speak_server_lib::app::App;
|
||||
use ox_speak_server_lib::config;
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let config = match config::Config::from_file("config.toml") {
|
||||
Ok(config) => config,
|
||||
Err(e) => panic!("Error loading configuration: {}", e)
|
||||
};
|
||||
println!("Configuration loaded: {:?}", config);
|
||||
|
||||
let app = App::init(config).await;
|
||||
app.run().await;
|
||||
}
|
||||
45
src/models/attachment.rs
Normal file
45
src/models/attachment.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::prelude::async_trait::async_trait;
|
||||
use sea_orm::Set;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "attachment")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub message_id: Uuid,
|
||||
pub filename: String,
|
||||
pub file_size: i32,
|
||||
pub mime_type: String,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::message::Entity",
|
||||
from = "Column::MessageId",
|
||||
to = "super::message::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Message,
|
||||
}
|
||||
|
||||
impl Related<super::message::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Message.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/models/category.rs
Normal file
52
src/models/category.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::prelude::async_trait::async_trait;
|
||||
use sea_orm::Set;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "category")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub server_id: Uuid,
|
||||
pub name: String,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::channel::Entity")]
|
||||
Channel,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::server::Entity",
|
||||
from = "Column::ServerId",
|
||||
to = "super::server::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Server,
|
||||
}
|
||||
|
||||
impl Related<super::channel::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Channel.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::server::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Server.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/models/channel.rs
Normal file
77
src/models/channel.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::prelude::async_trait::async_trait;
|
||||
use sea_orm::Set;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "channel")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub server_id: Option<Uuid>,
|
||||
pub category_id: Option<Uuid>,
|
||||
pub position: i32,
|
||||
pub channel_type: i32,
|
||||
pub name: Option<String>,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::category::Entity",
|
||||
from = "Column::CategoryId",
|
||||
to = "super::category::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "SetNull"
|
||||
)]
|
||||
Category,
|
||||
#[sea_orm(has_many = "super::channel_user::Entity")]
|
||||
ChannelUser,
|
||||
#[sea_orm(has_many = "super::message::Entity")]
|
||||
Message,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::server::Entity",
|
||||
from = "Column::ServerId",
|
||||
to = "super::server::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Server,
|
||||
}
|
||||
|
||||
impl Related<super::category::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Category.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::channel_user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ChannelUser.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::message::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Message.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::server::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Server.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/models/channel_user.rs
Normal file
58
src/models/channel_user.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::prelude::async_trait::async_trait;
|
||||
use sea_orm::Set;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "channel_user")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub channel_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub role: String,
|
||||
pub joined_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::channel::Entity",
|
||||
from = "Column::ChannelId",
|
||||
to = "super::channel::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Channel,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::channel::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Channel.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/models/message.rs
Normal file
77
src/models/message.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::prelude::async_trait::async_trait;
|
||||
use sea_orm::Set;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "message")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub channel_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub content: String,
|
||||
pub created_at: DateTime,
|
||||
pub edited_at: Option<DateTime>,
|
||||
pub reply_to_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::attachment::Entity")]
|
||||
Attachment,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::channel::Entity",
|
||||
from = "Column::ChannelId",
|
||||
to = "super::channel::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Channel,
|
||||
#[sea_orm(
|
||||
belongs_to = "Entity",
|
||||
from = "Column::ReplyToId",
|
||||
to = "Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "SetNull"
|
||||
)]
|
||||
SelfRef,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::attachment::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Attachment.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::channel::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Channel.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/models/mod.rs
Normal file
12
src/models/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod attachment;
|
||||
pub mod category;
|
||||
pub mod channel;
|
||||
pub mod channel_user;
|
||||
pub mod message;
|
||||
pub mod server;
|
||||
pub mod server_user;
|
||||
pub mod user;
|
||||
10
src/models/prelude.rs
Normal file
10
src/models/prelude.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
pub use super::attachment::Entity as Attachment;
|
||||
pub use super::category::Entity as Category;
|
||||
pub use super::channel::Entity as Channel;
|
||||
pub use super::channel_user::Entity as ChannelUser;
|
||||
pub use super::message::Entity as Message;
|
||||
pub use super::server::Entity as Server;
|
||||
pub use super::server_user::Entity as ServerUser;
|
||||
pub use super::user::Entity as User;
|
||||
54
src/models/server.rs
Normal file
54
src/models/server.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::prelude::async_trait::async_trait;
|
||||
use sea_orm::Set;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "server")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub password: Option<String>,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::category::Entity")]
|
||||
Category,
|
||||
#[sea_orm(has_many = "super::channel::Entity")]
|
||||
Channel,
|
||||
#[sea_orm(has_many = "super::server_user::Entity")]
|
||||
ServerUser,
|
||||
}
|
||||
|
||||
impl Related<super::category::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Category.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::channel::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Channel.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::server_user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServerUser.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/models/server_user.rs
Normal file
59
src/models/server_user.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::prelude::async_trait::async_trait;
|
||||
use sea_orm::Set;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "server_user")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub server_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub username: Option<String>,
|
||||
pub joined_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::server::Entity",
|
||||
from = "Column::ServerId",
|
||||
to = "super::server::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Server,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::server::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Server.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/models/user.rs
Normal file
55
src/models/user.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::prelude::async_trait::async_trait;
|
||||
use sea_orm::Set;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "user")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
#[sea_orm(column_type = "Text", unique)]
|
||||
pub pub_key: String,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::channel_user::Entity")]
|
||||
ChannelUser,
|
||||
#[sea_orm(has_many = "super::message::Entity")]
|
||||
Message,
|
||||
#[sea_orm(has_many = "super::server_user::Entity")]
|
||||
ServerUser,
|
||||
}
|
||||
|
||||
impl Related<super::channel_user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ChannelUser.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::message::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Message.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::server_user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServerUser.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/network/http/context.rs
Normal file
20
src/network/http/context.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
use std::time::Instant;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::app::AppState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RequestContext {
|
||||
pub request_id: Uuid,
|
||||
pub started_at: Instant,
|
||||
pub method: axum::http::Method,
|
||||
pub uri: axum::http::Uri,
|
||||
pub user: Option<CurrentUser>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CurrentUser {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
}
|
||||
49
src/network/http/error.rs
Normal file
49
src/network/http/error.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use serde_json::json;
|
||||
use sea_orm::DbErr;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HTTPError {
|
||||
Database(DbErr),
|
||||
NotFound,
|
||||
BadRequest(String),
|
||||
InternalServerError(String),
|
||||
}
|
||||
|
||||
// Conversion automatique depuis DbErr (erreurs SeaORM)
|
||||
impl From<DbErr> for HTTPError {
|
||||
fn from(err: DbErr) -> Self {
|
||||
HTTPError::Database(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Conversion depuis ParseError (pour UUID, etc.)
|
||||
impl From<uuid::Error> for HTTPError {
|
||||
fn from(err: uuid::Error) -> Self {
|
||||
HTTPError::BadRequest(format!("Invalid UUID: {}", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Implémentation pour Axum : transformer AppError en réponse HTTP
|
||||
impl IntoResponse for HTTPError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, error_message) = match self {
|
||||
HTTPError::Database(err) => {
|
||||
eprintln!("Database error: {:?}", err);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Database error")
|
||||
}
|
||||
HTTPError::NotFound => (StatusCode::NOT_FOUND, "Resource not found"),
|
||||
HTTPError::BadRequest(msg) => {
|
||||
return (StatusCode::BAD_REQUEST, Json(json!({ "error": msg }))).into_response();
|
||||
}
|
||||
HTTPError::InternalServerError(msg) => {
|
||||
eprintln!("Internal error: {}", msg);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": msg }))).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
(status, Json(json!({ "error": error_message }))).into_response()
|
||||
}
|
||||
}
|
||||
56
src/network/http/middleware.rs
Normal file
56
src/network/http/middleware.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::Request,
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use std::time::Instant;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::app::AppState;
|
||||
use crate::network::http::context::{CurrentUser, RequestContext};
|
||||
|
||||
pub async fn context_middleware(
|
||||
State(app_state): State<AppState>,
|
||||
mut req: Request<axum::body::Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let request_id = Uuid::new_v4();
|
||||
let started_at = Instant::now();
|
||||
|
||||
// Infos "type Django request"
|
||||
let method = req.method().clone();
|
||||
let uri = req.uri().clone();
|
||||
|
||||
// Exemple: récupérer un user depuis un token (pseudo-code)
|
||||
// Ici je laisse volontairement une logique minimaliste/placeholder.
|
||||
// Le but: montrer où tu branches ta vraie auth.
|
||||
let user: Option<CurrentUser> = {
|
||||
let _maybe_auth = req
|
||||
.headers()
|
||||
.get(axum::http::header::AUTHORIZATION)
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
// TODO: vérifier token -> user_id -> charger en DB avec app_state
|
||||
// Some(CurrentUser { id: ..., username: ... })
|
||||
None
|
||||
};
|
||||
|
||||
// Injecte le contexte dans la requête
|
||||
req.extensions_mut().insert(RequestContext {
|
||||
request_id,
|
||||
started_at,
|
||||
method: method.clone(),
|
||||
uri: uri.clone(),
|
||||
user,
|
||||
});
|
||||
|
||||
println!(">>> Incoming [{}] {} {}", request_id, method, uri);
|
||||
|
||||
// Passe la requête au reste de la stack
|
||||
let resp = next.run(req).await;
|
||||
|
||||
println!("<<< Response [{}]: {}", request_id, resp.status());
|
||||
|
||||
resp
|
||||
}
|
||||
15
src/network/http/mod.rs
Normal file
15
src/network/http/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use axum::Router;
|
||||
use crate::app::AppState;
|
||||
|
||||
mod server;
|
||||
mod router;
|
||||
mod middleware;
|
||||
mod web;
|
||||
mod error;
|
||||
mod context;
|
||||
|
||||
pub use server::HTTPServer;
|
||||
pub use error::HTTPError;
|
||||
pub use context::RequestContext;
|
||||
|
||||
pub type AppRouter = Router<AppState>;
|
||||
12
src/network/http/router.rs
Normal file
12
src/network/http/router.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{middleware, Router};
|
||||
use crate::app::AppState;
|
||||
use crate::network::http::middleware::context_middleware;
|
||||
use crate::network::http::{web, AppRouter};
|
||||
|
||||
pub fn setup_route(app_state: AppState) -> Router {
|
||||
Router::new()
|
||||
.merge(web::setup_route())
|
||||
.layer(middleware::from_fn_with_state(app_state.clone(), context_middleware))
|
||||
.with_state(app_state)
|
||||
}
|
||||
24
src/network/http/server.rs
Normal file
24
src/network/http/server.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use crate::app::AppState;
|
||||
use crate::network::http::router::setup_route;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HTTPServer {
|
||||
bind_addr: SocketAddr,
|
||||
app_state: AppState
|
||||
}
|
||||
|
||||
impl HTTPServer {
|
||||
pub fn new(bind_addr: SocketAddr, app_state: AppState) -> Self {
|
||||
Self {bind_addr, app_state}
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> std::io::Result<()> {
|
||||
let route = setup_route(self.app_state.clone());
|
||||
let listener = TcpListener::bind(&self.bind_addr).await?;
|
||||
|
||||
axum::serve(listener, route).await
|
||||
}
|
||||
}
|
||||
87
src/network/http/web/api/category.rs
Normal file
87
src/network/http/web/api/category.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use axum::{Extension, Json};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::{Extensions, StatusCode};
|
||||
use axum::routing::{delete, get, post, put};
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
|
||||
use uuid::Uuid;
|
||||
use crate::app::AppState;
|
||||
use crate::models::category;
|
||||
use crate::network::http::{AppRouter, HTTPError};
|
||||
use crate::network::http::RequestContext;
|
||||
use crate::serializers::CategorySerializer;
|
||||
|
||||
pub fn setup_route() -> AppRouter {
|
||||
AppRouter::new()
|
||||
.route("/categories/", get(category_list))
|
||||
.route("/categories/{id}/", get(category_detail))
|
||||
.route("/categories/", post(category_create))
|
||||
.route("/categories/{id}/", put(category_update))
|
||||
.route("/categories/{id}/", delete(category_delete))
|
||||
}
|
||||
|
||||
pub async fn category_list(
|
||||
State(app_state): State<AppState>,
|
||||
Extension(ctx): Extension<RequestContext>
|
||||
) -> Result<Json<Vec<CategorySerializer>>, HTTPError> {
|
||||
let categories = category::Entity::find()
|
||||
.all(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
Ok(Json(categories.into_iter().map(CategorySerializer::from).collect()))
|
||||
}
|
||||
|
||||
pub async fn category_detail(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<Json<CategorySerializer>, HTTPError> {
|
||||
let category = category::Entity::find_by_id(id)
|
||||
.one(app_state.db.get_connection())
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
Ok(Json(CategorySerializer::from(category)))
|
||||
}
|
||||
|
||||
pub async fn category_create(
|
||||
State(app_state): State<AppState>,
|
||||
Json(serializer): Json<CategorySerializer>
|
||||
) -> Result<Json<CategorySerializer>, HTTPError> {
|
||||
let active = serializer.into_active_model();
|
||||
let category: category::Model = active.insert(app_state.db.get_connection()).await?;
|
||||
|
||||
Ok(Json(CategorySerializer::from(category)))
|
||||
}
|
||||
|
||||
pub async fn category_update(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(serializer): Json<CategorySerializer>,
|
||||
) -> Result<Json<CategorySerializer>, HTTPError> {
|
||||
let category = category::Entity::find_by_id(id)
|
||||
.one(app_state.db.get_connection())
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
let active = category.into_active_model();
|
||||
|
||||
let category: category::Model = serializer.apply_to_active_model(active)
|
||||
.update(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
Ok(Json(CategorySerializer::from(category)))
|
||||
}
|
||||
|
||||
pub async fn category_delete(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<StatusCode, HTTPError> {
|
||||
let result = category::Entity::delete_by_id(id)
|
||||
.exec(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
if result.rows_affected == 0 {
|
||||
Err(HTTPError::NotFound)
|
||||
} else {
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
}
|
||||
85
src/network/http/web/api/channel.rs
Normal file
85
src/network/http/web/api/channel.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::{delete, get, post, put};
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
|
||||
use uuid::Uuid;
|
||||
use crate::app::AppState;
|
||||
use crate::models::channel;
|
||||
use crate::network::http::{AppRouter, HTTPError};
|
||||
use crate::serializers::ChannelSerializer;
|
||||
|
||||
pub fn setup_route() -> AppRouter {
|
||||
AppRouter::new()
|
||||
.route("/channels/", get(channel_list))
|
||||
.route("/channels/{id}/", get(channel_detail))
|
||||
.route("/channels/", post(channel_create))
|
||||
.route("/channels/{id}/", put(channel_update))
|
||||
.route("/channels/{id}/", delete(channel_delete))
|
||||
}
|
||||
|
||||
pub async fn channel_list(
|
||||
State(app_state): State<AppState>
|
||||
) -> Result<Json<Vec<ChannelSerializer>>, HTTPError> {
|
||||
let channels = channel::Entity::find()
|
||||
.all(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
Ok(Json(channels.into_iter().map(ChannelSerializer::from).collect()))
|
||||
}
|
||||
|
||||
pub async fn channel_detail(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<Json<ChannelSerializer>, HTTPError> {
|
||||
let channel = channel::Entity::find_by_id(id)
|
||||
.one(app_state.db.get_connection())
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
Ok(Json(ChannelSerializer::from(channel)))
|
||||
}
|
||||
|
||||
pub async fn channel_create(
|
||||
State(app_state): State<AppState>,
|
||||
Json(serializer): Json<ChannelSerializer>
|
||||
) -> Result<Json<ChannelSerializer>, HTTPError> {
|
||||
let active = serializer.into_active_model();
|
||||
let channel: channel::Model = active.insert(app_state.db.get_connection()).await?;
|
||||
|
||||
Ok(Json(ChannelSerializer::from(channel)))
|
||||
}
|
||||
|
||||
pub async fn channel_update(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(serializer): Json<ChannelSerializer>,
|
||||
) -> Result<Json<ChannelSerializer>, HTTPError> {
|
||||
let channel = channel::Entity::find_by_id(id)
|
||||
.one(app_state.db.get_connection())
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
let active = channel.into_active_model();
|
||||
|
||||
let channel: channel::Model = serializer.apply_to_active_model(active)
|
||||
.update(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
Ok(Json(ChannelSerializer::from(channel)))
|
||||
}
|
||||
|
||||
pub async fn channel_delete(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<StatusCode, HTTPError> {
|
||||
let result = channel::Entity::delete_by_id(id)
|
||||
.exec(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
if result.rows_affected > 0 {
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
Err(HTTPError::NotFound)
|
||||
}
|
||||
}
|
||||
84
src/network/http/web/api/message.rs
Normal file
84
src/network/http/web/api/message.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::{delete, get, post, put};
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
|
||||
use uuid::Uuid;
|
||||
use crate::app::AppState;
|
||||
use crate::models::message;
|
||||
use crate::network::http::{AppRouter, HTTPError};
|
||||
use crate::serializers::MessageSerializer;
|
||||
|
||||
pub fn setup_route() -> AppRouter {
|
||||
AppRouter::new()
|
||||
.route("/messages/", get(message_list))
|
||||
.route("/messages/{id}/", get(message_detail))
|
||||
.route("/messages/", post(message_create))
|
||||
.route("/messages/{id}/", put(message_update))
|
||||
.route("/messages/{id}/", delete(message_delete))
|
||||
}
|
||||
|
||||
pub async fn message_list(
|
||||
State(app_state): State<AppState>
|
||||
) -> Result<Json<Vec<MessageSerializer>>, HTTPError> {
|
||||
let messages = message::Entity::find()
|
||||
.all(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
Ok(Json(messages.into_iter().map(MessageSerializer::from).collect()))
|
||||
}
|
||||
|
||||
pub async fn message_detail(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<Json<MessageSerializer>, HTTPError> {
|
||||
let message = message::Entity::find_by_id(id)
|
||||
.one(app_state.db.get_connection())
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
Ok(Json(MessageSerializer::from(message)))
|
||||
}
|
||||
|
||||
pub async fn message_create(
|
||||
State(app_state): State<AppState>,
|
||||
Json(serializer): Json<MessageSerializer>
|
||||
) -> Result<Json<MessageSerializer>, HTTPError> {
|
||||
let active = serializer.into_active_model();
|
||||
let message: message::Model = active.insert(app_state.db.get_connection()).await?;
|
||||
|
||||
Ok(Json(MessageSerializer::from(message)))
|
||||
}
|
||||
|
||||
pub async fn message_update(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(serializer): Json<MessageSerializer>,
|
||||
) -> Result<Json<MessageSerializer>, HTTPError> {
|
||||
let message = message::Entity::find_by_id(id)
|
||||
.one(app_state.db.get_connection())
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
let active = message.into_active_model();
|
||||
let message: message::Model = serializer.apply_to_active_model(active)
|
||||
.update(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
Ok(Json(MessageSerializer::from(message)))
|
||||
}
|
||||
|
||||
pub async fn message_delete(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<StatusCode, HTTPError> {
|
||||
let result = message::Entity::delete_by_id(id)
|
||||
.exec(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
if result.rows_affected == 0 {
|
||||
Err(HTTPError::NotFound)
|
||||
} else {
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
}
|
||||
15
src/network/http/web/api/mod.rs
Normal file
15
src/network/http/web/api/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use crate::network::http::AppRouter;
|
||||
|
||||
mod category;
|
||||
mod channel;
|
||||
mod message;
|
||||
mod server;
|
||||
|
||||
pub fn setup_route() -> AppRouter {
|
||||
|
||||
AppRouter::new()
|
||||
.nest("/category", category::setup_route())
|
||||
.nest("/channel", channel::setup_route())
|
||||
.nest("/message", message::setup_route())
|
||||
.nest("/server", server::setup_route())
|
||||
}
|
||||
85
src/network/http/web/api/server.rs
Normal file
85
src/network/http/web/api/server.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::{delete, get, post, put};
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
|
||||
use uuid::Uuid;
|
||||
use crate::app::AppState;
|
||||
use crate::models::server;
|
||||
use crate::network::http::{AppRouter, HTTPError};
|
||||
use crate::serializers::ServerSerializer;
|
||||
|
||||
pub fn setup_route() -> AppRouter {
|
||||
AppRouter::new()
|
||||
.route("/servers/", get(server_list))
|
||||
.route("/servers/{id}/", get(server_detail))
|
||||
.route("/servers/{id}/", post(server_create))
|
||||
.route("/servers/{id}/", put(server_update))
|
||||
.route("/servers/{id}/", delete(server_delete))
|
||||
}
|
||||
|
||||
pub async fn server_list(
|
||||
State(app_state): State<AppState>
|
||||
) -> Result<Json<Vec<ServerSerializer>>, HTTPError> {
|
||||
let servers = server::Entity::find()
|
||||
.all(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
Ok(Json(servers.into_iter().map(ServerSerializer::from).collect()))
|
||||
}
|
||||
|
||||
pub async fn server_detail(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<Json<ServerSerializer>, HTTPError> {
|
||||
let server = server::Entity::find_by_id(id)
|
||||
.one(app_state.db.get_connection())
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
Ok(Json(ServerSerializer::from(server)))
|
||||
}
|
||||
|
||||
pub async fn server_create(
|
||||
State(app_state): State<AppState>,
|
||||
Json(serializer): Json<ServerSerializer>
|
||||
) -> Result<Json<ServerSerializer>, HTTPError> {
|
||||
let active = serializer.into_active_model();
|
||||
let server: server::Model = active.insert(app_state.db.get_connection()).await?;
|
||||
|
||||
Ok(Json(ServerSerializer::from(server)))
|
||||
}
|
||||
|
||||
pub async fn server_update(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(serializer): Json<ServerSerializer>,
|
||||
) -> Result<Json<ServerSerializer>, HTTPError> {
|
||||
let server = server::Entity::find_by_id(id)
|
||||
.one(app_state.db.get_connection())
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
let active = server.into_active_model();
|
||||
|
||||
let server: server::Model = serializer.apply_to_active_model(active)
|
||||
.update(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
Ok(Json(ServerSerializer::from(server)))
|
||||
}
|
||||
|
||||
pub async fn server_delete(
|
||||
State(app_state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<StatusCode, HTTPError> {
|
||||
let result = server::Entity::delete_by_id(id)
|
||||
.exec(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
if result.rows_affected == 0 {
|
||||
Err(HTTPError::NotFound)
|
||||
} else {
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
}
|
||||
0
src/network/http/web/auth.rs
Normal file
0
src/network/http/web/auth.rs
Normal file
11
src/network/http/web/mod.rs
Normal file
11
src/network/http/web/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use axum::Router;
|
||||
use crate::app::AppState;
|
||||
use crate::network::http::AppRouter;
|
||||
|
||||
mod api;
|
||||
|
||||
|
||||
pub fn setup_route() -> AppRouter {
|
||||
AppRouter::new()
|
||||
.nest("/api", api::setup_route())
|
||||
}
|
||||
2
src/network/mod.rs
Normal file
2
src/network/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod udp;
|
||||
pub mod http;
|
||||
3
src/network/udp/mod.rs
Normal file
3
src/network/udp/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod server;
|
||||
|
||||
pub use server::UDPServer;
|
||||
114
src/network/udp/server.rs
Normal file
114
src/network/udp/server.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::thread::available_parallelism;
|
||||
use std::io;
|
||||
use tokio::net::UdpSocket;
|
||||
use parking_lot::RwLock;
|
||||
use tokio::task;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UDPServer {
|
||||
table_router: Arc<RwLock<HashMap<String, SocketAddr>>>,
|
||||
bind_addr: SocketAddr
|
||||
}
|
||||
|
||||
impl UDPServer {
|
||||
pub fn new(bind_addr: SocketAddr) -> Self {
|
||||
Self {
|
||||
table_router: Arc::new(RwLock::new(HashMap::new())),
|
||||
bind_addr
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
self.run_unix().await
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
self.run_windows().await
|
||||
}
|
||||
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
Err(io::Error::new(io::ErrorKind::Other, "Unsupported platform"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn run_unix(&self) -> io::Result<()> {
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
|
||||
let mut workers = Vec::new();
|
||||
|
||||
for id in available_parallelism() {
|
||||
let bind_addr = self.bind_addr.clone();
|
||||
|
||||
let domain = match bind_addr {
|
||||
SocketAddr::V4(_) => Domain::IPV4,
|
||||
SocketAddr::V6(_) => Domain::IPV6,
|
||||
};
|
||||
|
||||
let sock = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
|
||||
sock.set_reuse_address(true)?;
|
||||
sock.set_reuse_port(true)?;
|
||||
sock.bind(&bind_addr.into())?;
|
||||
|
||||
let std_sock = std::net::UdpSocket::from(sock);
|
||||
std_sock.set_nonblocking(true)?;
|
||||
let udp = UdpSocket::from_std(std_sock)?;
|
||||
|
||||
let buffer_size = 1500;
|
||||
let worker = task::spawn(async move {
|
||||
if let Err(e) = Self::worker_loop(udp, buffer_size).await {
|
||||
eprintln!("Worker loop error: {}", e);
|
||||
}
|
||||
});
|
||||
workers.push(worker);
|
||||
}
|
||||
|
||||
for worker in workers {
|
||||
let _ = worker.await;
|
||||
}
|
||||
println!("All UDP workers stopped.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn run_windows(&self) -> io::Result<()> {
|
||||
let udp = UdpSocket::bind(self.bind_addr).await?;
|
||||
let udp = Arc::new(udp);
|
||||
|
||||
let mut workers = Vec::with_capacity(self.workers);
|
||||
for id in 0..self.workers {
|
||||
let sock = udp.clone();
|
||||
let buf_size = 1500;
|
||||
let worker = task::spawn(async move {
|
||||
if let Err(e) = Self::worker_loop(udp, buffer_size) {
|
||||
eprintln!("Worker loop error: {}", e);
|
||||
}
|
||||
});
|
||||
workers.push(worker);
|
||||
}
|
||||
|
||||
for worker in workers {
|
||||
let _ = worker.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn worker_loop(socket: UdpSocket, buffer_size: usize) -> io::Result<()>{
|
||||
let mut buffer = vec![0u8; buffer_size];
|
||||
loop {
|
||||
let (size, peer) = socket.recv_from(&mut buffer).await?;
|
||||
Self::handle_packet(&socket, &buffer[..size]).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_packet(socket: &UdpSocket, packet: &[u8]){
|
||||
|
||||
}
|
||||
}
|
||||
26
src/repositories/mod.rs
Normal file
26
src/repositories/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use std::sync::Arc;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use crate::repositories::server::ServerRepository;
|
||||
|
||||
mod server;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RepositoryContext {
|
||||
db: DatabaseConnection,
|
||||
// pub events: EventBus, // si tu veux publier des events “post-save” plus tard
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Repositories {
|
||||
pub server: ServerRepository,
|
||||
}
|
||||
|
||||
impl Repositories {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
let context = Arc::new(RepositoryContext { db });
|
||||
|
||||
Self {
|
||||
server: ServerRepository {context: context.clone()},
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/repositories/server.rs
Normal file
33
src/repositories/server.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::sync::Arc;
|
||||
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait};
|
||||
use crate::models::server;
|
||||
use crate::repositories::RepositoryContext;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ServerRepository {
|
||||
pub context: Arc<RepositoryContext>
|
||||
}
|
||||
|
||||
impl ServerRepository {
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<server::Model>, DbErr> {
|
||||
server::Entity::find_by_id(id).one(&self.context.db).await
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: server::ActiveModel) -> Result<server::Model, DbErr> {
|
||||
let model = active.update(&self.context.db).await?;
|
||||
// plus tard: self.context.events.publish(...)
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: server::ActiveModel) -> Result<server::Model, DbErr> {
|
||||
let model = active.insert(&self.context.db).await?;
|
||||
// plus tard: emit post-save
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||
server::Entity::delete_by_id(id).exec(&self.context.db).await?;
|
||||
// plus tard: emit post-delete
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
48
src/serializers/category.rs
Normal file
48
src/serializers/category.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use uuid::Uuid;
|
||||
use crate::models::category;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CategorySerializer {
|
||||
#[serde(skip_deserializing)]
|
||||
pub id: Option<Uuid>,
|
||||
|
||||
pub server_id: Uuid,
|
||||
|
||||
pub name: String,
|
||||
|
||||
#[serde(skip_deserializing)]
|
||||
pub created_at: Option<String>,
|
||||
|
||||
#[serde(skip_deserializing)]
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl From<category::Model> for CategorySerializer {
|
||||
fn from(model: category::Model) -> Self {
|
||||
Self {
|
||||
id: Some(model.id),
|
||||
server_id: model.server_id,
|
||||
name: model.name,
|
||||
created_at: Some(model.created_at.to_string()),
|
||||
updated_at: Some(model.updated_at.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CategorySerializer {
|
||||
pub fn into_active_model(self) -> category::ActiveModel {
|
||||
category::ActiveModel {
|
||||
server_id: Set(self.server_id),
|
||||
name: Set(self.name),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_to_active_model(self, mut active_model: category::ActiveModel) -> category::ActiveModel {
|
||||
active_model.server_id = Set(self.server_id);
|
||||
active_model.name = Set(self.name);
|
||||
active_model
|
||||
}
|
||||
}
|
||||
57
src/serializers/channel.rs
Normal file
57
src/serializers/channel.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use uuid::Uuid;
|
||||
use crate::models::channel;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ChannelSerializer {
|
||||
#[serde(skip_deserializing)]
|
||||
pub id: Option<Uuid>,
|
||||
pub server_id: Option<Uuid>,
|
||||
pub category_id: Option<Uuid>,
|
||||
pub name: Option<String>,
|
||||
pub position: Option<i32>,
|
||||
pub channel_type: i32,
|
||||
|
||||
#[serde(skip_deserializing)]
|
||||
pub created_at: Option<String>,
|
||||
#[serde(skip_deserializing)]
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl From<channel::Model> for ChannelSerializer {
|
||||
fn from(model: channel::Model) -> Self {
|
||||
Self {
|
||||
id: Some(model.id),
|
||||
server_id: model.server_id,
|
||||
category_id: model.category_id,
|
||||
name: model.name,
|
||||
position: Some(model.position),
|
||||
channel_type: model.channel_type,
|
||||
created_at: Some(model.created_at.to_string()),
|
||||
updated_at: Some(model.updated_at.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelSerializer {
|
||||
pub fn into_active_model(self) -> channel::ActiveModel {
|
||||
channel::ActiveModel {
|
||||
server_id: Set(self.server_id),
|
||||
category_id: Set(self.category_id),
|
||||
name: Set(self.name),
|
||||
position: Set(self.position.unwrap_or(0)),
|
||||
channel_type: Set(self.channel_type),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_to_active_model(self, mut active_model: channel::ActiveModel) -> channel::ActiveModel {
|
||||
active_model.server_id = Set(self.server_id);
|
||||
active_model.category_id = Set(self.category_id);
|
||||
active_model.name = Set(self.name);
|
||||
active_model.position = Set(self.position.unwrap_or(0));
|
||||
active_model.channel_type = Set(self.channel_type);
|
||||
active_model
|
||||
}
|
||||
}
|
||||
49
src/serializers/message.rs
Normal file
49
src/serializers/message.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use uuid::Uuid;
|
||||
use crate::models::message;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MessageSerializer {
|
||||
#[serde(skip_deserializing)]
|
||||
pub id: Option<Uuid>,
|
||||
pub channel_id: Option<Uuid>,
|
||||
pub author_id: Option<Uuid>,
|
||||
pub content: String,
|
||||
|
||||
#[serde(skip_deserializing)]
|
||||
pub created_at: Option<String>,
|
||||
#[serde(skip_deserializing)]
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl From<message::Model> for MessageSerializer {
|
||||
fn from(model: message::Model) -> Self {
|
||||
Self {
|
||||
id: Some(model.id),
|
||||
channel_id: Some(model.channel_id),
|
||||
author_id: Some(model.user_id),
|
||||
content: model.content,
|
||||
created_at: Some(model.created_at.to_string()),
|
||||
updated_at: Some(model.edited_at.unwrap_or(model.created_at).to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageSerializer {
|
||||
pub fn into_active_model(self) -> message::ActiveModel {
|
||||
message::ActiveModel {
|
||||
channel_id: Set(self.channel_id.unwrap()),
|
||||
user_id: Set(self.author_id.unwrap()),
|
||||
content: Set(self.content),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_to_active_model(self, mut active_model: message::ActiveModel) -> message::ActiveModel {
|
||||
active_model.channel_id = Set(self.channel_id.unwrap());
|
||||
active_model.user_id = Set(self.author_id.unwrap());
|
||||
active_model.content = Set(self.content);
|
||||
active_model
|
||||
}
|
||||
}
|
||||
9
src/serializers/mod.rs
Normal file
9
src/serializers/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod server;
|
||||
mod category;
|
||||
mod channel;
|
||||
mod message;
|
||||
|
||||
pub use server::*;
|
||||
pub use category::*;
|
||||
pub use channel::*;
|
||||
pub use message::*;
|
||||
54
src/serializers/server.rs
Normal file
54
src/serializers/server.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use uuid::Uuid;
|
||||
use crate::models::server;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ServerSerializer {
|
||||
#[serde(skip_deserializing)]
|
||||
pub id: Option<Uuid>,
|
||||
|
||||
pub name: String,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub password: Option<String>,
|
||||
|
||||
#[serde(skip_deserializing)]
|
||||
pub created_at: Option<String>,
|
||||
|
||||
#[serde(skip_deserializing)]
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
// On part du Model (données « propres » venant de la BDD),
|
||||
// pas de l'ActiveModel (qui contient des ActiveValue<T>).
|
||||
impl From<server::Model> for ServerSerializer {
|
||||
fn from(model: server::Model) -> Self {
|
||||
Self {
|
||||
id: Some(model.id),
|
||||
name: model.name,
|
||||
password: model.password,
|
||||
created_at: Some(model.created_at.to_string()),
|
||||
updated_at: Some(model.updated_at.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerSerializer {
|
||||
/// équivalent de `create()` d’un serializer DRF
|
||||
pub fn into_active_model(self) -> server::ActiveModel {
|
||||
server::ActiveModel {
|
||||
name: Set(self.name),
|
||||
// champ Option<String> -> tu peux le passer directement à Set(...)
|
||||
password: Set(self.password),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// équivalent de `update(instance, validated_data)` en DRF
|
||||
pub fn apply_to_active_model(self, mut active_model: server::ActiveModel) -> server::ActiveModel {
|
||||
active_model.name = Set(self.name);
|
||||
active_model.password = Set(self.password);
|
||||
active_model
|
||||
}
|
||||
}
|
||||
1
src/utils/mod.rs
Normal file
1
src/utils/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod toolbox;
|
||||
9
src/utils/toolbox.rs
Normal file
9
src/utils/toolbox.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub fn number_of_cpus() -> usize {
|
||||
match std::thread::available_parallelism() {
|
||||
Ok(n) => n.get(),
|
||||
Err(_) => {
|
||||
eprintln!("Warning: Could not determine number of CPUs, defaulting to 1");
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user