This commit is contained in:
2026-03-01 03:33:36 +01:00
parent 68f16055a8
commit e982ea372f
26 changed files with 496 additions and 550 deletions

212
Cargo.lock generated
View File

@@ -375,6 +375,19 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]]
name = "bcrypt"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abaf6da45c74385272ddf00e1ac074c7d8a6c1a1dda376902bd6a427522a8b2c"
dependencies = [
"base64",
"blowfish",
"getrandom 0.3.4",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "bigdecimal" name = "bigdecimal"
version = "0.4.9" version = "0.4.9"
@@ -441,6 +454,16 @@ dependencies = [
"piper", "piper",
] ]
[[package]]
name = "blowfish"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
dependencies = [
"byteorder",
"cipher",
]
[[package]] [[package]]
name = "borsh" name = "borsh"
version = "1.6.0" version = "1.6.0"
@@ -1137,8 +1160,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1575,6 +1600,21 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "jsonwebtoken"
version = "9.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
dependencies = [
"base64",
"js-sys",
"pem",
"ring",
"serde",
"serde_json",
"simple_asn1",
]
[[package]] [[package]]
name = "kv-log-macro" name = "kv-log-macro"
version = "1.0.7" version = "1.0.7"
@@ -1713,6 +1753,15 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.60.2",
]
[[package]] [[package]]
name = "num-bigint" name = "num-bigint"
version = "0.4.6" version = "0.4.6"
@@ -1741,9 +1790,9 @@ dependencies = [
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]] [[package]]
name = "num-integer" name = "num-integer"
@@ -1827,9 +1876,11 @@ dependencies = [
"argon2", "argon2",
"axum", "axum",
"base64", "base64",
"bcrypt",
"chrono", "chrono",
"env_logger", "env_logger",
"futures-util", "futures-util",
"jsonwebtoken",
"log", "log",
"migration", "migration",
"parking_lot", "parking_lot",
@@ -1842,7 +1893,10 @@ dependencies = [
"ssh-key", "ssh-key",
"tokio", "tokio",
"toml", "toml",
"tower",
"tower-http", "tower-http",
"tracing",
"tracing-subscriber",
"uuid", "uuid",
"validator", "validator",
] ]
@@ -1925,6 +1979,16 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "pem"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
dependencies = [
"base64",
"serde_core",
]
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@@ -2281,6 +2345,20 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.16",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rkyv" name = "rkyv"
version = "0.7.45" version = "0.7.45"
@@ -2720,6 +2798,18 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "simple_asn1"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d"
dependencies = [
"num-bigint",
"num-traits",
"thiserror",
"time",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.11" version = "0.4.11"
@@ -3127,30 +3217,30 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.44" version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
"num-conv", "num-conv",
"powerfmt", "powerfmt",
"serde", "serde_core",
"time-core", "time-core",
"time-macros", "time-macros",
] ]
[[package]] [[package]]
name = "time-core" name = "time-core"
version = "0.1.6" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.24" version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [ dependencies = [
"num-conv", "num-conv",
"time-core", "time-core",
@@ -3307,8 +3397,10 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"bytes", "bytes",
"futures-util",
"http", "http",
"http-body", "http-body",
"http-body-util",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
"tower-layer", "tower-layer",
@@ -3358,6 +3450,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
] ]
[[package]] [[package]]
@@ -3367,12 +3471,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [ dependencies = [
"matchers", "matchers",
"nu-ansi-term",
"once_cell", "once_cell",
"regex-automata", "regex-automata",
"sharded-slab", "sharded-slab",
"smallvec",
"thread_local", "thread_local",
"tracing", "tracing",
"tracing-core", "tracing-core",
"tracing-log",
] ]
[[package]] [[package]]
@@ -3431,6 +3538,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.7" version = "2.5.7"
@@ -3504,6 +3617,12 @@ dependencies = [
"syn 2.0.110", "syn 2.0.110",
] ]
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "value-bag" name = "value-bag"
version = "1.12.0" version = "1.12.0"
@@ -3689,6 +3808,15 @@ dependencies = [
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.60.2" version = "0.60.2"
@@ -3722,6 +3850,22 @@ dependencies = [
"windows_x86_64_msvc 0.48.5", "windows_x86_64_msvc 0.48.5",
] ]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.53.5" version = "0.53.5"
@@ -3732,7 +3876,7 @@ dependencies = [
"windows_aarch64_gnullvm 0.53.1", "windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1", "windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1", "windows_i686_gnu 0.53.1",
"windows_i686_gnullvm", "windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1", "windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1", "windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1", "windows_x86_64_gnullvm 0.53.1",
@@ -3745,6 +3889,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.53.1" version = "0.53.1"
@@ -3757,6 +3907,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.53.1" version = "0.53.1"
@@ -3769,12 +3925,24 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.53.1" version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]] [[package]]
name = "windows_i686_gnullvm" name = "windows_i686_gnullvm"
version = "0.53.1" version = "0.53.1"
@@ -3787,6 +3955,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.53.1" version = "0.53.1"
@@ -3799,6 +3973,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.53.1" version = "0.53.1"
@@ -3811,6 +3991,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.53.1" version = "0.53.1"
@@ -3823,6 +4009,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.53.1" version = "0.53.1"

View File

@@ -44,8 +44,8 @@ tokio = { version = "1.49", features = ["full"] }
axum = { version = "0.8.8", features = ["macros", "ws"] } axum = { version = "0.8.8", features = ["macros", "ws"] }
#utoipa = "5.4" #utoipa = "5.4"
#utoipa-swagger-ui = { version = "9.0", features = ["axum"] } #utoipa-swagger-ui = { version = "9.0", features = ["axum"] }
#tower = "0.5" tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.6", features = ["trace", "cors", "timeout"] } tower-http = { version = "0.6", features = ["trace", "cors", "timeout", "catch-panic"] }
# UDP # UDP
socket2 = "0.6" socket2 = "0.6"
@@ -56,6 +56,8 @@ migration = { path = "migration" }
# logs # logs
log = "0.4" log = "0.4"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
env_logger = "0.11.8" env_logger = "0.11.8"
# utils # utils
@@ -72,3 +74,5 @@ rand = "0.9"
ssh-key = { version = "0.6", features = ["default", "crypto"] } ssh-key = { version = "0.6", features = ["default", "crypto"] }
base64 = "0.22" base64 = "0.22"
argon2 = "0.5.3" argon2 = "0.5.3"
jsonwebtoken = "9.3.1"
bcrypt = "0.17.0"

View File

@@ -23,14 +23,17 @@ impl App {
pub async fn init(config: Config) -> Self { pub async fn init(config: Config) -> Self {
let event_bus = EventBus::new(1024); let event_bus = EventBus::new(1024);
let db = Database::init(&config.database_url()).await.expect("Failed to initialize database"); let db = Database::init(&config.database_url())
.await
.expect("Failed to initialize database");
let repositories = Repositories::new(db.get_connection(), event_bus.clone()); let repositories = Repositories::new(db.get_connection(), event_bus.clone());
let state = AppState{ let state = AppState {
db: db.clone(), db: db.clone(),
event_bus: event_bus.clone(), event_bus: event_bus.clone(),
repositories: repositories.clone(), repositories: repositories.clone(),
clients: Clients::new() clients: Clients::new(),
config: config.clone(),
}; };
let udp_server = UDPServer::new(config.bind_addr()); let udp_server = UDPServer::new(config.bind_addr());
@@ -86,7 +89,5 @@ impl App {
println!("Nettoyage et fermeture de l'application."); println!("Nettoyage et fermeture de l'application.");
} }
async fn shutdown(&self) { async fn shutdown(&self) {}
}
} }

View File

@@ -1,3 +1,4 @@
use crate::config::Config;
use crate::database::Database; use crate::database::Database;
use crate::event_bus::EventBus; use crate::event_bus::EventBus;
use crate::hub::Clients; use crate::hub::Clients;
@@ -9,6 +10,7 @@ pub struct AppState {
pub event_bus: EventBus, pub event_bus: EventBus,
pub repositories: Repositories, pub repositories: Repositories,
pub clients: Clients, pub clients: Clients,
pub config: Config,
} }
impl AppState { impl AppState {

View File

@@ -1,7 +1,7 @@
use crate::models::channel; use crate::models::channel;
use crate::models::channel::ChannelType; use crate::models::channel::ChannelType;
use sea_orm::Set; use sea_orm::Set;
use serde::Serialize; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@@ -27,11 +27,11 @@ impl From<channel::Model> for ChannelResponse {
} }
} }
#[derive(Debug, Serialize)] #[derive(Debug, Deserialize)]
pub struct CreateChannelRequest { pub struct CreateChannelRequest {
pub server_id: Option<Uuid>, pub server_id: Option<Uuid>,
pub category_id: Option<Uuid>, pub category_id: Option<Uuid>,
pub position: i32, pub position: Option<i32>,
pub channel_type: ChannelType, pub channel_type: ChannelType,
pub name: Option<String>, pub name: Option<String>,
} }
@@ -41,10 +41,21 @@ impl From<CreateChannelRequest> for channel::ActiveModel {
Self { Self {
server_id: Set(request.server_id), server_id: Set(request.server_id),
category_id: Set(request.category_id), category_id: Set(request.category_id),
position: Set(request.position), position: Set(request.position.unwrap_or(0)),
channel_type: Set(request.channel_type), channel_type: Set(request.channel_type),
name: Set(request.name), name: Set(request.name),
..Default::default() ..Default::default()
} }
} }
} }
impl CreateChannelRequest {
pub fn apply_to(self, mut am: channel::ActiveModel) -> channel::ActiveModel {
am.server_id = Set(self.server_id);
am.category_id = Set(self.category_id);
am.position = Set(self.position.unwrap_or(0));
am.channel_type = Set(self.channel_type);
am.name = Set(self.name);
am
}
}

View File

@@ -28,13 +28,19 @@ pub struct CreateMessageRequest {
pub content: String, pub content: String,
} }
impl From<CreateMessageRequest> for message::ActiveModel { impl CreateMessageRequest {
fn from(request: CreateMessageRequest) -> Self { pub fn into_active_model(self, author_id: Uuid) -> message::ActiveModel {
Self { message::ActiveModel {
channel_id: Set(request.channel_id), channel_id: Set(self.channel_id),
user_id: Set(Uuid::new_v4()), user_id: Set(author_id),
content: Set(request.content), content: Set(self.content),
..Default::default() ..Default::default()
} }
} }
pub fn apply_to(self, mut am: message::ActiveModel) -> message::ActiveModel {
am.channel_id = Set(self.channel_id);
am.content = Set(self.content);
am
}
} }

View File

@@ -7,6 +7,7 @@ use uuid::Uuid;
pub struct UserResponse { pub struct UserResponse {
pub id: Uuid, pub id: Uuid,
pub username: String, pub username: String,
pub pub_key: String,
} }
impl From<user::Model> for UserResponse { impl From<user::Model> for UserResponse {
@@ -14,6 +15,7 @@ impl From<user::Model> for UserResponse {
Self { Self {
id: model.id, id: model.id,
username: model.username, username: model.username,
pub_key: model.pub_key,
} }
} }
} }
@@ -31,3 +33,10 @@ impl From<CreateUserRequest> for user::ActiveModel {
} }
} }
} }
impl CreateUserRequest {
pub fn apply_to(self, mut am: user::ActiveModel) -> user::ActiveModel {
am.username = Set(self.username);
am
}
}

View File

@@ -8,6 +8,5 @@ pub mod event_bus;
pub mod hub; pub mod hub;
pub mod models; pub mod models;
pub mod repositories; pub mod repositories;
pub mod serializers;
pub mod interfaces; pub mod interfaces;

View File

@@ -1,14 +1,18 @@
use ox_speak_server_lib::app::App; use ox_speak_server_lib::app::App;
use ox_speak_server_lib::config; use ox_speak_server_lib::config;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init(); tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "ox_speak_server=info,tower_http=info".into()),
)
.init();
let config = match config::Config::from_file("config.toml") { let config = match config::Config::from_file("config.toml") {
Ok(config) => config, Ok(config) => config,
Err(e) => panic!("Error loading configuration: {}", e) Err(e) => panic!("Error loading configuration: {}", e),
}; };
println!("Configuration loaded: {:?}", config); println!("Configuration loaded: {:?}", config);

View File

@@ -1,8 +1,8 @@
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use axum::Json; use axum::Json;
use serde_json::json;
use sea_orm::DbErr; use sea_orm::DbErr;
use serde_json::json;
#[derive(Debug)] #[derive(Debug)]
pub enum HTTPError { pub enum HTTPError {
@@ -10,6 +10,7 @@ pub enum HTTPError {
NotFound, NotFound,
BadRequest(String), BadRequest(String),
InternalServerError(String), InternalServerError(String),
Unauthorized,
} }
// Conversion automatique depuis DbErr (erreurs SeaORM) // Conversion automatique depuis DbErr (erreurs SeaORM)
@@ -35,12 +36,17 @@ impl IntoResponse for HTTPError {
(StatusCode::INTERNAL_SERVER_ERROR, "Database error") (StatusCode::INTERNAL_SERVER_ERROR, "Database error")
} }
HTTPError::NotFound => (StatusCode::NOT_FOUND, "Resource not found"), HTTPError::NotFound => (StatusCode::NOT_FOUND, "Resource not found"),
HTTPError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized"),
HTTPError::BadRequest(msg) => { HTTPError::BadRequest(msg) => {
return (StatusCode::BAD_REQUEST, Json(json!({ "error": msg }))).into_response(); return (StatusCode::BAD_REQUEST, Json(json!({ "error": msg }))).into_response();
} }
HTTPError::InternalServerError(msg) => { HTTPError::InternalServerError(msg) => {
eprintln!("Internal error: {}", msg); eprintln!("Internal error: {}", msg);
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": msg }))).into_response(); return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": msg })),
)
.into_response();
} }
}; };

View File

@@ -1,14 +1,11 @@
use axum::{ use axum::{extract::State, http::Request, middleware::Next, response::Response};
extract::State,
http::Request,
middleware::Next,
response::Response,
};
use std::time::Instant; use std::time::Instant;
use tracing::info;
use uuid::Uuid; use uuid::Uuid;
use crate::app::AppState; use crate::app::AppState;
use crate::network::http::context::{CurrentUser, RequestContext}; use crate::network::http::context::{CurrentUser, RequestContext};
use crate::utils::auth::verify_jwt;
pub async fn context_middleware( pub async fn context_middleware(
State(app_state): State<AppState>, State(app_state): State<AppState>,
@@ -22,20 +19,27 @@ pub async fn context_middleware(
let method = req.method().clone(); let method = req.method().clone();
let uri = req.uri().clone(); let uri = req.uri().clone();
// Exemple: récupérer un user depuis un token (pseudo-code) // Authentification par JWT
// Ici je laisse volontairement une logique minimaliste/placeholder.
// Le but: montrer où tu branches ta vraie auth.
let user: Option<CurrentUser> = { let user: Option<CurrentUser> = {
let _maybe_auth = req req.headers()
.headers()
.get(axum::http::header::AUTHORIZATION) .get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok()); .and_then(|v| v.to_str().ok())
.and_then(|auth_header| {
// TODO: vérifier token -> user_id -> charger en DB avec app_state if auth_header.starts_with("Bearer ") {
// Some(CurrentUser { id: ..., username: ... }) Some(&auth_header[7..])
} else {
None None
}
})
.and_then(|token| verify_jwt(token, &app_state.config.jwt.secret).ok())
.map(|claims| CurrentUser {
id: claims.user_id,
username: claims.username,
})
}; };
let user_id = user.as_ref().map(|u| u.id);
// Injecte le contexte dans la requête // Injecte le contexte dans la requête
req.extensions_mut().insert(RequestContext { req.extensions_mut().insert(RequestContext {
request_id, request_id,
@@ -45,12 +49,14 @@ pub async fn context_middleware(
user, user,
}); });
println!(">>> Incoming [{}] {} {}", request_id, method, uri); info!(
request_id = %request_id,
user_id = ?user_id,
method = %method,
uri = %uri,
"Incoming request"
);
// Passe la requête au reste de la stack // Passe la requête au reste de la stack
let resp = next.run(req).await; next.run(req).await
println!("<<< Response [{}]: {}", request_id, resp.status());
resp
} }

View File

@@ -1,14 +1,25 @@
use std::sync::Arc;
use axum::{middleware, Router}; use axum::{middleware, Router};
use tower::ServiceBuilder;
use tower_http::catch_panic::CatchPanicLayer;
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
use crate::app::AppState; use crate::app::AppState;
use crate::network::http::middleware::context_middleware; use crate::network::http::middleware::context_middleware;
use crate::network::http::{web, AppRouter}; use crate::network::http::web;
pub fn setup_route(app_state: AppState) -> Router { pub fn setup_route(app_state: AppState) -> Router {
let middleware_stack = ServiceBuilder::new()
.layer(CatchPanicLayer::new())
.layer(TraceLayer::new_for_http())
.layer(middleware::from_fn_with_state(
app_state.clone(),
context_middleware,
));
Router::new() Router::new()
.merge(web::setup_route()) .merge(web::setup_route())
.layer(middleware::from_fn_with_state(app_state.clone(), context_middleware)) .layer(middleware_stack)
.with_state(app_state) .with_state(app_state)
.layer(CorsLayer::permissive()) .layer(CorsLayer::permissive())
} }

View File

@@ -1,13 +1,53 @@
use crate::app::AppState; use crate::app::AppState;
use crate::network::http::{AppRouter, HTTPError}; use crate::network::http::{AppRouter, HTTPError};
use crate::utils::auth::create_jwt;
use crate::utils::toolbox::ssh_generate_challenge; use crate::utils::toolbox::ssh_generate_challenge;
use axum::extract::State; use axum::extract::State;
use axum::routing::post;
use axum::Json; use axum::Json;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ssh_key::{Algorithm as SshAlgorithm, PublicKey, Signature};
fn setup_route() -> AppRouter { pub fn setup_route() -> AppRouter {
AppRouter::new() AppRouter::new()
.route("/login", post(login))
.route("/ssh-challenge", post(ssh_challenge))
}
#[derive(Deserialize)]
pub struct LoginRequest {
username: String,
password: String,
}
#[derive(Serialize)]
pub struct LoginResponse {
token: String,
username: String,
}
pub async fn login(
State(state): State<AppState>,
Json(payload): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, HTTPError> {
let user = state
.repositories
.user
.check_password(payload.username.clone(), payload.password)
.await
.map_err(|_| HTTPError::Unauthorized)?;
let token = create_jwt(
user.id,
&user.username,
&state.config.jwt.secret,
state.config.jwt.expiration,
)
.map_err(|e| HTTPError::InternalServerError("Failed to generate token".to_string()))?;
Ok(Json(LoginResponse {
token,
username: user.username,
}))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -24,82 +64,14 @@ pub async fn ssh_challenge(
State(state): State<AppState>, State(state): State<AppState>,
Json(payload): Json<SshChallengeRequest>, Json(payload): Json<SshChallengeRequest>,
) -> Result<Json<SshChallengeResponse>, HTTPError> { ) -> Result<Json<SshChallengeResponse>, HTTPError> {
log::info!( let _user = state
"POST /auth/ssh-challenge - Challenge request for user: {}",
payload.username
);
let user = state
.repositories .repositories
.user .user
.get_by_username(payload.username.clone()) .get_by_username(payload.username.clone())
.await? .await?
.ok_or_else(|| { .ok_or_else(|| HTTPError::NotFound)?;
log::warn!(
"POST /auth/ssh-challenge - User not found: {}",
payload.username
);
HTTPError::NotFound
})?;
let challenge = ssh_generate_challenge(32); let challenge = ssh_generate_challenge(32);
log::info!(
"POST /auth/ssh-challenge - Challenge generated for user: {}",
payload.username
);
// todo : stocker le challenge dans AppState
// bien penser à ajouter un délai d'expiration
// songer à mettre une session id ?
// Peut être que l'utilisateur se connectera depuis différent device
// state.store_challenge(&payload.username, challenge.clone())
Ok(Json(SshChallengeResponse { challenge })) Ok(Json(SshChallengeResponse { challenge }))
} }
#[derive(Deserialize)]
enum SignatureAlgorithm {
#[serde(rename = "rsa")]
Rsa,
#[serde(rename = "ed25519")]
Ed25519,
#[serde(rename = "ecdsa")]
Ecdsa,
}
impl SignatureAlgorithm {
fn to_ssh_algorithm(&self) -> SshAlgorithm {
match self {
SignatureAlgorithm::Rsa => SshAlgorithm::Rsa { hash: None },
SignatureAlgorithm::Ed25519 => SshAlgorithm::Ed25519,
SignatureAlgorithm::Ecdsa => SshAlgorithm::Ecdsa {
curve: ssh_key::EcdsaCurve::NistP256,
},
}
}
}
#[derive(Deserialize)]
struct SshVerifyRequest {
username: String,
signature: String,
algorithm: SignatureAlgorithm,
}
#[derive(Serialize)]
struct SshVerifyResponse {
// todo : remplir avec la réponse jwt - à établir après la construction jwt
}
pub async fn ssh_verify() {}
#[derive(Deserialize)]
pub struct LoginRequest {
username: String,
password: String,
}
#[derive(Serialize)]
pub struct SshChallengeResponse {
challenge: String,
}

View File

@@ -3,9 +3,8 @@ use crate::interfaces::http::dto::category::{CategoryResponse, CreateCategoryReq
use crate::models::category; use crate::models::category;
use crate::network::http::RequestContext; use crate::network::http::RequestContext;
use crate::network::http::{AppRouter, HTTPError}; use crate::network::http::{AppRouter, HTTPError};
use crate::serializers::CategorySerializer;
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use axum::http::{Extensions, StatusCode}; use axum::http::StatusCode;
use axum::routing::{delete, get, post, put}; use axum::routing::{delete, get, post, put};
use axum::{Extension, Json}; use axum::{Extension, Json};
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel}; use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
@@ -31,8 +30,6 @@ pub async fn category_list(
Extension(_ctx): Extension<RequestContext>, Extension(_ctx): Extension<RequestContext>,
Query(query): Query<CategoryQuery>, Query(query): Query<CategoryQuery>,
) -> Result<Json<Vec<CategoryResponse>>, HTTPError> { ) -> Result<Json<Vec<CategoryResponse>>, HTTPError> {
log::info!("GET /categories/ - Query: server_id={:?}", query.server_id);
let categories = if let Some(server_id) = query.server_id { let categories = if let Some(server_id) = query.server_id {
app_state app_state
.repositories .repositories
@@ -43,7 +40,6 @@ pub async fn category_list(
app_state.repositories.category.get_all().await? app_state.repositories.category.get_all().await?
}; };
log::info!("GET /categories/ - Found {} categories", categories.len());
Ok(Json( Ok(Json(
categories.into_iter().map(CategoryResponse::from).collect(), categories.into_iter().map(CategoryResponse::from).collect(),
)) ))
@@ -53,19 +49,13 @@ pub async fn category_detail(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<Json<CategoryResponse>, HTTPError> { ) -> Result<Json<CategoryResponse>, HTTPError> {
log::info!("GET /categories/{id}/ - Fetching category: {}", id);
let category = app_state let category = app_state
.repositories .repositories
.category .category
.get_by_id(id) .get_by_id(id)
.await? .await?
.ok_or_else(|| { .ok_or_else(|| HTTPError::NotFound)?;
log::warn!("GET /categories/{id}/ - Category not found: {}", id);
HTTPError::NotFound
})?;
log::info!("GET /categories/{id}/ - Category found: {}", id);
Ok(Json(CategoryResponse::from(category))) Ok(Json(CategoryResponse::from(category)))
} }
@@ -73,18 +63,9 @@ pub async fn category_create(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Json(serializer): Json<CreateCategoryRequest>, Json(serializer): Json<CreateCategoryRequest>,
) -> Result<Json<CategoryResponse>, HTTPError> { ) -> Result<Json<CategoryResponse>, HTTPError> {
log::info!(
"POST /categories/ - Creating category: {:?}",
serializer.name
);
let active: category::ActiveModel = serializer.into(); let active: category::ActiveModel = serializer.into();
let category: category::Model = active.insert(app_state.db.get_connection()).await?; let category: category::Model = active.insert(app_state.db.get_connection()).await?;
log::info!(
"POST /categories/ - Category created with id: {}",
category.id
);
Ok(Json(CategoryResponse::from(category))) Ok(Json(CategoryResponse::from(category)))
} }
@@ -93,15 +74,10 @@ pub async fn category_update(
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Json(serializer): Json<CreateCategoryRequest>, Json(serializer): Json<CreateCategoryRequest>,
) -> Result<Json<CategoryResponse>, HTTPError> { ) -> Result<Json<CategoryResponse>, HTTPError> {
log::info!("PUT /categories/{id}/ - Updating category: {}", id);
let category = category::Entity::find_by_id(id) let category = category::Entity::find_by_id(id)
.one(app_state.db.get_connection()) .one(app_state.db.get_connection())
.await? .await?
.ok_or_else(|| { .ok_or_else(|| HTTPError::NotFound)?;
log::warn!("PUT /categories/{id}/ - Category not found: {}", id);
HTTPError::NotFound
})?;
let active = category.into_active_model(); let active = category.into_active_model();
@@ -111,7 +87,6 @@ pub async fn category_update(
.update(app_state.db.get_connection()) .update(app_state.db.get_connection())
.await?; .await?;
log::info!("PUT /categories/{id}/ - Category updated: {}", id);
Ok(Json(CategoryResponse::from(category))) Ok(Json(CategoryResponse::from(category)))
} }
@@ -119,17 +94,13 @@ pub async fn category_delete(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<StatusCode, HTTPError> { ) -> Result<StatusCode, HTTPError> {
log::info!("DELETE /categories/{id}/ - Deleting category: {}", id);
let result = category::Entity::delete_by_id(id) let result = category::Entity::delete_by_id(id)
.exec(app_state.db.get_connection()) .exec(app_state.db.get_connection())
.await?; .await?;
if result.rows_affected == 0 { if result.rows_affected == 0 {
log::warn!("DELETE /categories/{id}/ - Category not found: {}", id);
Err(HTTPError::NotFound) Err(HTTPError::NotFound)
} else { } else {
log::info!("DELETE /categories/{id}/ - Category deleted: {}", id);
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
} }

View File

@@ -1,13 +1,13 @@
use axum::Json; use crate::app::AppState;
use crate::interfaces::http::dto::channel::{ChannelResponse, CreateChannelRequest};
use crate::models::channel;
use crate::network::http::{AppRouter, HTTPError};
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::routing::{delete, get, post, put}; use axum::routing::{delete, get, post, put};
use axum::Json;
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel}; use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
use uuid::Uuid; 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 { pub fn setup_route() -> AppRouter {
AppRouter::new() AppRouter::new()
@@ -19,42 +19,44 @@ pub fn setup_route() -> AppRouter {
} }
pub async fn channel_list( pub async fn channel_list(
State(app_state): State<AppState> State(app_state): State<AppState>,
) -> Result<Json<Vec<ChannelSerializer>>, HTTPError> { ) -> Result<Json<Vec<ChannelResponse>>, HTTPError> {
let channels = channel::Entity::find() let channels = channel::Entity::find()
.all(app_state.db.get_connection()) .all(app_state.db.get_connection())
.await?; .await?;
Ok(Json(channels.into_iter().map(ChannelSerializer::from).collect())) Ok(Json(
channels.into_iter().map(ChannelResponse::from).collect(),
))
} }
pub async fn channel_detail( pub async fn channel_detail(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Path(id): Path<Uuid> Path(id): Path<Uuid>,
) -> Result<Json<ChannelSerializer>, HTTPError> { ) -> Result<Json<ChannelResponse>, HTTPError> {
let channel = channel::Entity::find_by_id(id) let channel = channel::Entity::find_by_id(id)
.one(app_state.db.get_connection()) .one(app_state.db.get_connection())
.await? .await?
.ok_or(HTTPError::NotFound)?; .ok_or(HTTPError::NotFound)?;
Ok(Json(ChannelSerializer::from(channel))) Ok(Json(ChannelResponse::from(channel)))
} }
pub async fn channel_create( pub async fn channel_create(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Json(serializer): Json<ChannelSerializer> Json(dto): Json<CreateChannelRequest>,
) -> Result<Json<ChannelSerializer>, HTTPError> { ) -> Result<Json<ChannelResponse>, HTTPError> {
let active = serializer.into_active_model(); let active: channel::ActiveModel = dto.into();
let channel: channel::Model = active.insert(app_state.db.get_connection()).await?; let channel: channel::Model = active.insert(app_state.db.get_connection()).await?;
Ok(Json(ChannelSerializer::from(channel))) Ok(Json(ChannelResponse::from(channel)))
} }
pub async fn channel_update( pub async fn channel_update(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Json(serializer): Json<ChannelSerializer>, Json(dto): Json<CreateChannelRequest>,
) -> Result<Json<ChannelSerializer>, HTTPError> { ) -> Result<Json<ChannelResponse>, HTTPError> {
let channel = channel::Entity::find_by_id(id) let channel = channel::Entity::find_by_id(id)
.one(app_state.db.get_connection()) .one(app_state.db.get_connection())
.await? .await?
@@ -62,16 +64,17 @@ pub async fn channel_update(
let active = channel.into_active_model(); let active = channel.into_active_model();
let channel: channel::Model = serializer.apply_to_active_model(active) let channel: channel::Model = dto
.apply_to(active)
.update(app_state.db.get_connection()) .update(app_state.db.get_connection())
.await?; .await?;
Ok(Json(ChannelSerializer::from(channel))) Ok(Json(ChannelResponse::from(channel)))
} }
pub async fn channel_delete( pub async fn channel_delete(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Path(id): Path<Uuid> Path(id): Path<Uuid>,
) -> Result<StatusCode, HTTPError> { ) -> Result<StatusCode, HTTPError> {
let result = channel::Entity::delete_by_id(id) let result = channel::Entity::delete_by_id(id)
.exec(app_state.db.get_connection()) .exec(app_state.db.get_connection())

View File

@@ -1,13 +1,13 @@
use axum::Json; use crate::app::AppState;
use crate::interfaces::http::dto::message::{CreateMessageRequest, MessageResponse};
use crate::models::message;
use crate::network::http::{AppRouter, HTTPError, RequestContext};
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::routing::{delete, get, post, put}; use axum::routing::{delete, get, post, put};
use axum::{Extension, Json};
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel}; use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
use uuid::Uuid; 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 { pub fn setup_route() -> AppRouter {
AppRouter::new() AppRouter::new()
@@ -19,58 +19,63 @@ pub fn setup_route() -> AppRouter {
} }
pub async fn message_list( pub async fn message_list(
State(app_state): State<AppState> State(app_state): State<AppState>,
) -> Result<Json<Vec<MessageSerializer>>, HTTPError> { ) -> Result<Json<Vec<MessageResponse>>, HTTPError> {
let messages = message::Entity::find() let messages = message::Entity::find()
.all(app_state.db.get_connection()) .all(app_state.db.get_connection())
.await?; .await?;
Ok(Json(messages.into_iter().map(MessageSerializer::from).collect())) Ok(Json(
messages.into_iter().map(MessageResponse::from).collect(),
))
} }
pub async fn message_detail( pub async fn message_detail(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Path(id): Path<Uuid> Path(id): Path<Uuid>,
) -> Result<Json<MessageSerializer>, HTTPError> { ) -> Result<Json<MessageResponse>, HTTPError> {
let message = message::Entity::find_by_id(id) let message = message::Entity::find_by_id(id)
.one(app_state.db.get_connection()) .one(app_state.db.get_connection())
.await? .await?
.ok_or(HTTPError::NotFound)?; .ok_or(HTTPError::NotFound)?;
Ok(Json(MessageSerializer::from(message))) Ok(Json(MessageResponse::from(message)))
} }
pub async fn message_create( pub async fn message_create(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Json(serializer): Json<MessageSerializer> Extension(ctx): Extension<RequestContext>,
) -> Result<Json<MessageSerializer>, HTTPError> { Json(dto): Json<CreateMessageRequest>,
let active = serializer.into_active_model(); ) -> Result<Json<MessageResponse>, HTTPError> {
let author_id = ctx.user.map(|u| u.id).unwrap_or_else(Uuid::new_v4);
let active = dto.into_active_model(author_id);
let message: message::Model = active.insert(app_state.db.get_connection()).await?; let message: message::Model = active.insert(app_state.db.get_connection()).await?;
Ok(Json(MessageSerializer::from(message))) Ok(Json(MessageResponse::from(message)))
} }
pub async fn message_update( pub async fn message_update(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Json(serializer): Json<MessageSerializer>, Json(dto): Json<CreateMessageRequest>,
) -> Result<Json<MessageSerializer>, HTTPError> { ) -> Result<Json<MessageResponse>, HTTPError> {
let message = message::Entity::find_by_id(id) let message = message::Entity::find_by_id(id)
.one(app_state.db.get_connection()) .one(app_state.db.get_connection())
.await? .await?
.ok_or(HTTPError::NotFound)?; .ok_or(HTTPError::NotFound)?;
let active = message.into_active_model(); let active = message.into_active_model();
let message: message::Model = serializer.apply_to_active_model(active) let message: message::Model = dto
.apply_to(active)
.update(app_state.db.get_connection()) .update(app_state.db.get_connection())
.await?; .await?;
Ok(Json(MessageSerializer::from(message))) Ok(Json(MessageResponse::from(message)))
} }
pub async fn message_delete( pub async fn message_delete(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Path(id): Path<Uuid> Path(id): Path<Uuid>,
) -> Result<StatusCode, HTTPError> { ) -> Result<StatusCode, HTTPError> {
let result = message::Entity::delete_by_id(id) let result = message::Entity::delete_by_id(id)
.exec(app_state.db.get_connection()) .exec(app_state.db.get_connection())

View File

@@ -1,18 +1,18 @@
use crate::network::http::AppRouter; use crate::network::http::AppRouter;
mod auth;
mod category; mod category;
mod channel; mod channel;
mod message; mod message;
mod server; mod server;
mod user; mod user;
mod auth;
pub fn setup_route() -> AppRouter { pub fn setup_route() -> AppRouter {
AppRouter::new() AppRouter::new()
.nest("/category", category::setup_route()) .nest("/category", category::setup_route())
.nest("/channel", channel::setup_route()) .nest("/channel", channel::setup_route())
.nest("/message", message::setup_route()) .nest("/message", message::setup_route())
.nest("/server", server::setup_route()) .nest("/server", server::setup_route())
.nest("/user", user::setup_route()) .nest("/user", user::setup_route())
.nest("/auth", auth::setup_route())
} }

View File

@@ -1,42 +1,48 @@
use axum::{Extension, Json}; use crate::app::AppState;
use crate::interfaces::http::dto::user::UserResponse;
use crate::network::http::{AppRouter, HTTPError};
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::routing::get; use axum::routing::get;
use axum::{Extension, Json};
use uuid::Uuid; use uuid::Uuid;
use crate::app::AppState;
use crate::network::http::{AppRouter, HTTPError};
use crate::serializers::UserSerializer;
pub fn setup_route() -> AppRouter { pub fn setup_route() -> AppRouter {
AppRouter::new() AppRouter::new()
.route("/users/", get(user_list)) .route("/me", get(get_me))
.route("/users/{id}/", get(user_detail)) .route("/", get(user_list))
.route("/{id}", get(user_detail))
}
pub async fn get_me(
Extension(ctx): Extension<crate::network::http::RequestContext>,
) -> Result<Json<UserResponse>, HTTPError> {
let user = ctx.user.ok_or(HTTPError::Unauthorized)?;
Ok(Json(UserResponse {
id: user.id,
username: user.username,
pub_key: "".to_string(), // On peut laisser vide ou charger en DB si besoin
}))
} }
pub async fn user_list( pub async fn user_list(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Extension(_ctx): Extension<crate::network::http::RequestContext> Extension(_ctx): Extension<crate::network::http::RequestContext>,
) -> Result<Json<Vec<UserSerializer>>, HTTPError> { ) -> Result<Json<Vec<UserResponse>>, HTTPError> {
log::info!("GET /users/ - Fetching user list");
let users = app_state.repositories.user.get_all().await?; let users = app_state.repositories.user.get_all().await?;
log::info!("GET /users/ - Found {} users", users.len());
Ok(Json(users.into_iter().map(UserSerializer::from).collect())) Ok(Json(users.into_iter().map(UserResponse::from).collect()))
} }
pub async fn user_detail( pub async fn user_detail(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Path(id): Path<Uuid> Path(id): Path<Uuid>,
) -> Result<Json<UserSerializer>, HTTPError> { ) -> Result<Json<UserResponse>, HTTPError> {
log::info!("GET /users/{id}/ - Fetching user with id: {}", id); let user = app_state
.repositories
.user
.get_by_id(id)
.await?
.ok_or_else(|| HTTPError::NotFound)?;
let user = app_state.repositories.user Ok(Json(UserResponse::from(user)))
.get_by_id(id).await?
.ok_or_else(|| {
log::warn!("GET /users/{id}/ - User not found: {}", id);
HTTPError::NotFound
})?;
log::info!("GET /users/{id}/ - User found: {}", id);
Ok(Json(UserSerializer::from(user)))
} }

View File

@@ -1,48 +0,0 @@
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
}
}

View File

@@ -1,58 +0,0 @@
use serde::{Serialize, Deserialize};
use sea_orm::ActiveValue::Set;
use uuid::Uuid;
use crate::models::channel;
use crate::models::channel::ChannelType;
#[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: ChannelType,
#[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
}
}

View File

@@ -1,49 +0,0 @@
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
}
}

View File

@@ -1,21 +0,0 @@
mod server;
mod category;
mod channel;
mod message;
mod user;
trait BaseSerializer<M>
where
M: ActiveModelTrait,
{
fn into_active_model(self) -> M;
fn apply_to_active_model(self, active_model: M) -> M;
}
use sea_orm::ActiveModelTrait;
pub use server::*;
pub use category::*;
pub use channel::*;
pub use message::*;
pub use user::*;

View File

@@ -1,91 +0,0 @@
use serde::{Serialize, Deserialize};
use sea_orm::ActiveValue::Set;
use uuid::Uuid;
use crate::models::server;
use crate::repositories::types::{ServerExplorerItem, ServerTree};
use super::{CategorySerializer, ChannelSerializer};
#[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()` dun 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
}
}
#[derive(Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TreeItemSerializer {
Category {
#[serde(flatten)]
category: CategorySerializer,
channels: Vec<ChannelSerializer>
},
Channel(ChannelSerializer)
}
#[derive(Serialize)]
#[serde(transparent)]
pub struct ServerTreeSerializer {
pub items: Vec<TreeItemSerializer>,
}
impl From<ServerTree> for ServerTreeSerializer {
fn from(layout: ServerTree) -> Self {
Self {
items: layout.items.into_iter().map(|item| match item {
ServerExplorerItem::Category(cat, chans) => {
TreeItemSerializer::Category {
category: CategorySerializer::from(cat),
channels: chans.into_iter().map(ChannelSerializer::from).collect(),
}
},
ServerExplorerItem::Channel(chan) => {
TreeItemSerializer::Channel(ChannelSerializer::from(chan))
}
}).collect(),
}
}
}

View File

@@ -1,44 +0,0 @@
use sea_orm::ActiveValue::Set;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::models::{category, user};
#[derive(Serialize, Deserialize)]
pub struct UserSerializer {
#[serde(skip_deserializing)]
pub id: Option<Uuid>,
pub username: String,
pub pub_key: String,
#[serde(skip_deserializing)]
pub created_at: String,
#[serde(skip_deserializing)]
pub updated_at: String,
}
impl From<user::Model> for UserSerializer {
fn from(model: user::Model) -> Self {
Self {
id: Some(model.id),
username: model.username,
pub_key: model.pub_key,
created_at: model.created_at.to_string(),
updated_at: model.updated_at.to_string(),
}
}
}
impl UserSerializer {
pub fn into_active_model(self) -> user::ActiveModel {
user::ActiveModel {
username: Set(self.username),
pub_key: Set(self.pub_key),
..Default::default()
}
}
pub fn apply_to_active_model(self, mut active_model: user::ActiveModel) -> user::ActiveModel { active_model.username = Set(self.username);
active_model.pub_key = Set(self.pub_key);
active_model
}
}

48
src/utils/auth.rs Normal file
View File

@@ -0,0 +1,48 @@
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub user_id: Uuid, // User ID
pub expire_at: usize, // Expiration time
pub created_at: usize, // Issued at
pub username: String,
}
pub fn create_jwt(
user_id: Uuid,
username: &str,
secret: &str,
expiration_seconds: u64,
) -> Result<String, jsonwebtoken::errors::Error> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
let claims = Claims {
user_id: user_id,
expire_at: (now + expiration_seconds) as usize,
created_at: now as usize,
username: username.to_string(),
};
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_ref()),
)
}
pub fn verify_jwt(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let validation = Validation::default();
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_ref()),
&validation,
)?;
Ok(token_data.claims)
}

View File

@@ -1,3 +1,4 @@
pub mod auth;
pub mod password; pub mod password;
pub mod ssh_auth; pub mod ssh_auth;
pub mod toolbox; pub mod toolbox;