pre-metrics
This commit is contained in:
Generated
+167
-38
@@ -154,9 +154,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "arrow"
|
||||
version = "57.3.0"
|
||||
version = "57.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4754a624e5ae42081f464514be454b39711daae0458906dacde5f4c632f33a8"
|
||||
checksum = "3bd47f2a6ddc39244bd722a27ee5da66c03369d087b9e024eafdb03e98b98ea7"
|
||||
dependencies = [
|
||||
"arrow-arith",
|
||||
"arrow-array",
|
||||
@@ -172,9 +172,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arrow-arith"
|
||||
version = "57.3.0"
|
||||
version = "57.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7b3141e0ec5145a22d8694ea8b6d6f69305971c4fa1c1a13ef0195aef2d678b"
|
||||
checksum = "7c7bbd679c5418b8639b92be01f361d60013c4906574b578b77b63c78356594c"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -186,9 +186,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arrow-array"
|
||||
version = "57.3.0"
|
||||
version = "57.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c8955af33b25f3b175ee10af580577280b4bd01f7e823d94c7cdef7cf8c9aef"
|
||||
checksum = "c8a4ab47b3f3eac60f7fd31b81e9028fda018607bcc63451aca4f2b755269862"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"arrow-buffer",
|
||||
@@ -204,9 +204,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arrow-buffer"
|
||||
version = "57.3.0"
|
||||
version = "57.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c697ddca96183182f35b3a18e50b9110b11e916d7b7799cbfd4d34662f2c56c2"
|
||||
checksum = "0d18b89b4c4f4811d0858175e79541fe98e33e18db3b011708bc287b1240593f"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"half",
|
||||
@@ -216,9 +216,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arrow-cast"
|
||||
version = "57.3.0"
|
||||
version = "57.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "646bbb821e86fd57189c10b4fcdaa941deaf4181924917b0daa92735baa6ada5"
|
||||
checksum = "722b5c41dd1d14d0a879a1bce92c6fe33f546101bb2acce57a209825edd075b3"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -237,9 +237,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arrow-data"
|
||||
version = "57.3.0"
|
||||
version = "57.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fdd994a9d28e6365aa78e15da3f3950c0fdcea6b963a12fa1c391afb637b304"
|
||||
checksum = "c1683705c63dcf0d18972759eda48489028cbbff67af7d6bef2c6b7b74ab778a"
|
||||
dependencies = [
|
||||
"arrow-buffer",
|
||||
"arrow-schema",
|
||||
@@ -250,9 +250,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arrow-ord"
|
||||
version = "57.3.0"
|
||||
version = "57.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d8f1870e03d4cbed632959498bcc84083b5a24bded52905ae1695bd29da45b"
|
||||
checksum = "082342947d4e5a2bcccf029a0a0397e21cb3bb8421edd9571d34fb5dd2670256"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -263,9 +263,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arrow-row"
|
||||
version = "57.3.0"
|
||||
version = "57.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18228633bad92bff92a95746bbeb16e5fc318e8382b75619dec26db79e4de4c0"
|
||||
checksum = "e3a931b520a2a5e22033e01a6f2486b4cdc26f9106b759abeebc320f125e94d7"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -276,15 +276,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arrow-schema"
|
||||
version = "57.3.0"
|
||||
version = "57.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68"
|
||||
checksum = "e4cf0d4a6609679e03002167a61074a21d7b1ad9ea65e462b2c0a97f8a3b2bc6"
|
||||
|
||||
[[package]]
|
||||
name = "arrow-select"
|
||||
version = "57.3.0"
|
||||
version = "57.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68bf3e3efbd1278f770d67e5dc410257300b161b93baedb3aae836144edcaf4b"
|
||||
checksum = "0b320d86a9806923663bb0fd9baa65ecaba81cb0cd77ff8c1768b9716b4ef891"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"arrow-array",
|
||||
@@ -296,9 +296,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arrow-string"
|
||||
version = "57.3.0"
|
||||
version = "57.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85e968097061b3c0e9fe3079cf2e703e487890700546b5b0647f60fca1b5a8d8"
|
||||
checksum = "b493e99162e5764077e7823e50ba284858d365922631c7aaefe9487b1abd02c2"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -739,8 +739,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
@@ -834,9 +836,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.15.22"
|
||||
version = "0.15.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c"
|
||||
checksum = "f316c6237b2d38be61949ecd15268a4c6ca32570079394a2444d9ce2c72a72d8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"convert_case",
|
||||
@@ -1050,6 +1052,7 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
@@ -1285,6 +1288,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -1484,7 +1493,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1492,12 +1501,15 @@ name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
@@ -1508,6 +1520,15 @@ dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
@@ -1606,9 +1627,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.11"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5"
|
||||
checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
@@ -1794,7 +1815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.0",
|
||||
"hashbrown 0.17.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
@@ -1872,6 +1893,23 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "10.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"getrandom 0.2.17",
|
||||
"js-sys",
|
||||
"pem",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"signature",
|
||||
"simple_asn1",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kv-log-macro"
|
||||
version = "1.0.7"
|
||||
@@ -2252,11 +2290,15 @@ dependencies = [
|
||||
name = "oxspeak_server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"bitflags",
|
||||
"chrono",
|
||||
"config",
|
||||
"event_bus",
|
||||
"jsonwebtoken",
|
||||
"log",
|
||||
"migration",
|
||||
"parking_lot",
|
||||
@@ -2266,10 +2308,13 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"utoipa",
|
||||
"uuid",
|
||||
"validator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2327,6 +2372,16 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@@ -3243,6 +3298,18 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@@ -3318,7 +3385,7 @@ dependencies = [
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hashbrown 0.15.5",
|
||||
"hashlink",
|
||||
"hashlink 0.10.0",
|
||||
"indexmap",
|
||||
"log",
|
||||
"memchr",
|
||||
@@ -3786,6 +3853,24 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
@@ -3979,6 +4064,36 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
|
||||
dependencies = [
|
||||
"idna",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"url",
|
||||
"validator_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator_derive"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"once_cell",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
@@ -4320,9 +4435,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
|
||||
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -4438,13 +4553,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust2"
|
||||
version = "0.10.4"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9"
|
||||
checksum = "631a50d867fafb7093e709d75aaee9e0e0d5deb934021fcea25ac2fe09edc51e"
|
||||
dependencies = [
|
||||
"arraydeque",
|
||||
"encoding_rs",
|
||||
"hashlink",
|
||||
"hashlink 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4498,9 +4613,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
|
||||
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
@@ -4522,6 +4637,20 @@ name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
|
||||
@@ -29,3 +29,10 @@ utoipa = { version = "5", features = ["uuid"] }
|
||||
log = "0.4"
|
||||
bitflags = "2.11.1"
|
||||
argon2 = { version = "0.6.0-rc.8", features = ["password-hash"] }
|
||||
jsonwebtoken = "10.3.0"
|
||||
tower = { version = "0.5", features = ["util"] }
|
||||
tower-http = { version = "0.6", features = ["catch-panic", "cors", "trace"] }
|
||||
chrono = "0.4.44"
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
async-trait = "0.1.89"
|
||||
anyhow = "1.0.102"
|
||||
@@ -12,6 +12,7 @@ udp_port = 8080
|
||||
# DSN for database
|
||||
# SQLite
|
||||
url = "sqlite://oxspeak.db"
|
||||
#url = "sqlite::memory:"
|
||||
# PostgreSQL
|
||||
# url = "postgresql://user:passwd@localhost:5432/db_name"
|
||||
# MySQL
|
||||
|
||||
@@ -85,6 +85,7 @@ const DEFAULT_CAPACITY: usize = 64;
|
||||
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct EventBus {
|
||||
/// Channels indexed by exact topic.
|
||||
channels: RwLock<HashMap<String, broadcast::Sender<AnyEvent>>>,
|
||||
|
||||
@@ -45,6 +45,12 @@ impl MigrationTrait for Migration {
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Alias::new("is_default"))
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod password;
|
||||
pub mod token;
|
||||
@@ -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)
|
||||
}
|
||||
+106
-1
@@ -2,9 +2,14 @@ pub mod state;
|
||||
|
||||
use crate::config::AppConfig;
|
||||
use crate::database::Database;
|
||||
use crate::http::server::HttpServer;
|
||||
use crate::repositories::Repositories;
|
||||
use crate::udp::server::UdpServer;
|
||||
use event_bus::EventBus;
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
pub use state::AppState;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct App {
|
||||
pub state: AppState,
|
||||
@@ -12,14 +17,53 @@ pub struct App {
|
||||
|
||||
impl App {
|
||||
pub async fn build(config: AppConfig) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
// Initialize database connection
|
||||
let db_manager = Database::init(&config.database.url).await?;
|
||||
let db = db_manager.get_connection().clone();
|
||||
|
||||
// Run database migrations
|
||||
Migrator::up(&db, None).await?;
|
||||
|
||||
// Initialize EventBus
|
||||
let event_bus = Arc::new(EventBus::with_capacity(1024));
|
||||
|
||||
// Initialize shared repositories
|
||||
let repositories = Repositories::new(db.clone(), event_bus.clone());
|
||||
|
||||
// Init one server if no one exist
|
||||
let default_server = match repositories.server.get_default().await? {
|
||||
Some(server) => server,
|
||||
None => {
|
||||
let new_server = repositories
|
||||
.server
|
||||
.create_with_args("default".to_string(), true)
|
||||
.await?;
|
||||
tracing::info!("Initialized default server");
|
||||
new_server
|
||||
}
|
||||
};
|
||||
|
||||
// Init Token if no user exist
|
||||
let init_token = if repositories.user.count().await? == 0 {
|
||||
let token = Uuid::new_v4();
|
||||
println!("+------------------------------------------------------------+");
|
||||
println!("| NO USER FOUND IN DATABASE |");
|
||||
println!("| Use the following token to create the first admin user: |");
|
||||
println!("| |");
|
||||
println!("| TOKEN: {} |", token);
|
||||
println!("| |");
|
||||
println!("+------------------------------------------------------------+");
|
||||
Some(token)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let state = AppState {
|
||||
db,
|
||||
config: Arc::new(config),
|
||||
repositories,
|
||||
init_token,
|
||||
default_server: Arc::new(default_server),
|
||||
};
|
||||
|
||||
Ok(Self { state })
|
||||
@@ -28,6 +72,67 @@ impl App {
|
||||
pub async fn run(self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing::info!("Starting services...");
|
||||
|
||||
tokio::select! {}
|
||||
let config = self.state.config.clone();
|
||||
|
||||
// Initialize HTTP Server
|
||||
let (http_server, http_shutdown_tx) = HttpServer::new(&config.network, self.state.clone());
|
||||
|
||||
// Initialize UDP service
|
||||
let (udp_server, udp_shutdown_tx) = UdpServer::new(&config.network);
|
||||
|
||||
// On lance les serveurs dans des tâches séparées
|
||||
let mut http_handle = tokio::spawn(http_server.run());
|
||||
let mut udp_handle = tokio::spawn(udp_server.run());
|
||||
|
||||
// On arbitre : soit un signal arrive, soit une tâche se termine (erreur/crash)
|
||||
tokio::select! {
|
||||
res = &mut http_handle => {
|
||||
tracing::error!("HTTP server stopped unexpectedly: {:?}", res);
|
||||
}
|
||||
res = &mut udp_handle => {
|
||||
tracing::error!("UDP server stopped unexpectedly: {:?}", res);
|
||||
}
|
||||
_ = Self::shutdown_signal() => {
|
||||
tracing::info!("Shutdown signal received, initiating graceful shutdown...");
|
||||
}
|
||||
}
|
||||
|
||||
// Dans tous les cas (Ctrl-C ou crash d'un service), on demande l'arrêt global
|
||||
let _ = http_shutdown_tx.send(());
|
||||
let _ = udp_shutdown_tx.send(());
|
||||
|
||||
// On attend que tout le monde ait fini de nettoyer
|
||||
// (Note: join! supporte les handles déjà terminés ou annulés)
|
||||
let _ = tokio::join!(http_handle, udp_handle);
|
||||
|
||||
tracing::info!("Closed the runtime application.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
let ctrl_c = async {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to install Ctrl+C handler");
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let terminate = async {
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("failed to install signal handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let terminate = std::future::pending::<()>();
|
||||
|
||||
tokio::select! {
|
||||
_ = ctrl_c => {},
|
||||
_ = terminate => {},
|
||||
}
|
||||
|
||||
tracing::info!("Shutdown signal received");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::config::AppConfig;
|
||||
use crate::models::server;
|
||||
use crate::repositories::Repositories;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -6,4 +8,9 @@ use std::sync::Arc;
|
||||
pub struct AppState {
|
||||
pub db: DatabaseConnection,
|
||||
pub config: Arc<AppConfig>,
|
||||
pub repositories: Repositories,
|
||||
pub init_token: Option<uuid::Uuid>,
|
||||
pub default_server: Arc<server::Model>,
|
||||
}
|
||||
|
||||
impl AppState {}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use sea_orm::{ConnectOptions, Database as SeaDatabase, DatabaseConnection, DbErr};
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
pub connection: DatabaseConnection,
|
||||
@@ -9,7 +8,13 @@ pub struct Database {
|
||||
|
||||
impl Database {
|
||||
pub async fn init(dsn: &str) -> Result<Self, DbErr> {
|
||||
let mut opt = ConnectOptions::new(dsn);
|
||||
let mut final_dsn = dsn.to_string();
|
||||
if dsn.starts_with("sqlite:") && !dsn.contains('?') && dsn != "sqlite::memory:" {
|
||||
final_dsn = format!("{}?mode=rwc", dsn);
|
||||
}
|
||||
println!("{}", final_dsn);
|
||||
|
||||
let mut opt = ConnectOptions::new(final_dsn);
|
||||
opt.max_connections(100)
|
||||
.min_connections(5)
|
||||
.connect_timeout(Duration::from_secs(8))
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
use super::error::HTTPError;
|
||||
use crate::models::user;
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::http::request::Parts;
|
||||
use std::ops::Deref;
|
||||
use std::time::Instant;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
/// Représente l'utilisateur actuellement authentifié, enveloppant le modèle de base de données.
|
||||
///
|
||||
/// **Philosophie (vs Django) :**
|
||||
/// Au lieu de passer une `request` entière, on demande `user: CurrentUser` dans la signature
|
||||
/// de la vue. C'est un **contrat** : si l'utilisateur n'est pas là, la vue n'est pas appelée (401).
|
||||
///
|
||||
/// **Usage :**
|
||||
/// ```rust
|
||||
/// pub async fn ma_vue(user: CurrentUser) {
|
||||
/// if user.is_superuser { ... }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CurrentUser(pub user::Model);
|
||||
|
||||
impl Deref for CurrentUser {
|
||||
type Target = user::Model;
|
||||
|
||||
/// Permet d'accéder aux champs du modèle (`user.username`, etc.) directement
|
||||
/// sur l'objet `CurrentUser`, sans avoir à faire `user.0.username`.
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Le "Moteur" derrière la magie des signatures de fonction d'Axum.
|
||||
///
|
||||
/// Implémenter ce trait permet à Axum d'extraire automatiquement l'utilisateur
|
||||
/// depuis les extensions de la requête (injectées par le middleware).
|
||||
impl<S> FromRequestParts<S> for CurrentUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = HTTPError;
|
||||
|
||||
/// Cette méthode est appelée par Axum AVANT d'exécuter votre vue.
|
||||
/// 1. On cherche le `RequestContext` dans les extensions.
|
||||
/// 2. On vérifie si un utilisateur y est présent.
|
||||
/// 3. Si oui, on le retourne (succès).
|
||||
/// 4. Si non, on retourne une erreur 401 (rejet de la requête).
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
// On récupère le contexte injecté par le middleware
|
||||
let context = parts
|
||||
.extensions
|
||||
.get::<RequestContext>()
|
||||
.ok_or(HTTPError::Unauthorized)?;
|
||||
|
||||
// On retourne l'utilisateur cloné s'il existe, sinon on rejette avec Unauthorized
|
||||
context.user.clone().ok_or(HTTPError::Unauthorized)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use sea_orm::DbErr;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ValidationErrorResponse {
|
||||
pub errors: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HTTPError {
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] DbErr),
|
||||
|
||||
#[error("Resource not found")]
|
||||
NotFound,
|
||||
|
||||
#[error("Validation failed")]
|
||||
UnprocessableEntity(HashMap<String, Vec<String>>),
|
||||
|
||||
#[error("Bad request: {0}")]
|
||||
BadRequest(String),
|
||||
|
||||
#[error("Internal server error: {0}")]
|
||||
InternalServerError(String),
|
||||
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("Forbidden")]
|
||||
Forbidden,
|
||||
|
||||
#[error("Invalid UUID: {0}")]
|
||||
UuidError(#[from] uuid::Error),
|
||||
|
||||
#[error("Internal server error: {0}")]
|
||||
Internal(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
// 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::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
HTTPError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden"),
|
||||
HTTPError::UuidError(err) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "error": format!("Invalid UUID: {}", err) })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
HTTPError::UnprocessableEntity(errors) => {
|
||||
return (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Json(ValidationErrorResponse { errors }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
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();
|
||||
}
|
||||
HTTPError::Internal(err) => {
|
||||
tracing::error!(error = ?err, "An unexpected error occurred");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
|
||||
}
|
||||
};
|
||||
|
||||
(status, Json(json!({ "error": error_message }))).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl HTTPError {
|
||||
pub fn validation_error(field: &str, message: &str) -> Self {
|
||||
let mut errors = HashMap::new();
|
||||
errors.insert(field.to_string(), vec![message.to_string()]);
|
||||
HTTPError::UnprocessableEntity(errors)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use axum::{extract::State, http::Request, middleware::Next, response::Response};
|
||||
use std::time::Instant;
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::context::{CurrentUser, RequestContext};
|
||||
use crate::auth::token::verify_jwt;
|
||||
use crate::core::AppState;
|
||||
|
||||
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();
|
||||
|
||||
// Authentification par JWT
|
||||
let user: Option<CurrentUser> = match req
|
||||
.headers()
|
||||
.get(axum::http::header::AUTHORIZATION)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|auth_header| {
|
||||
if auth_header.starts_with("Bearer ") {
|
||||
Some(&auth_header[7..])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.and_then(|token| verify_jwt(token, &app_state.config.jwt.secret).ok())
|
||||
{
|
||||
Some(claims) => app_state
|
||||
.repositories
|
||||
.user
|
||||
.get_by_id(claims.user_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(CurrentUser),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let user_id = user.as_ref().map(|u| u.id);
|
||||
|
||||
// Injecte le contexte dans la requête (espace de stockage partagé)
|
||||
// C'est ce qui permettra aux extracteurs comme 'CurrentUser' de retrouver ces données plus tard.
|
||||
req.extensions_mut().insert(RequestContext {
|
||||
request_id,
|
||||
started_at,
|
||||
method: method.clone(),
|
||||
uri: uri.clone(),
|
||||
user,
|
||||
});
|
||||
|
||||
info!(
|
||||
request_id = %request_id,
|
||||
user_id = ?user_id,
|
||||
method = %method,
|
||||
uri = %uri,
|
||||
"Incoming request"
|
||||
);
|
||||
|
||||
// Passe la requête au reste de la stack
|
||||
next.run(req).await
|
||||
}
|
||||
@@ -1 +1,11 @@
|
||||
use crate::core::AppState;
|
||||
use axum::Router;
|
||||
|
||||
pub mod context;
|
||||
pub mod error;
|
||||
pub mod metrics;
|
||||
mod middleware;
|
||||
pub mod server;
|
||||
pub mod validation;
|
||||
|
||||
pub type OxRouter = Router<AppState>;
|
||||
|
||||
+156
-11
@@ -1,23 +1,168 @@
|
||||
use std::net::SocketAddr;
|
||||
//! Serveur HTTP axum.
|
||||
//!
|
||||
//! [`HttpServer`] encapsule la configuration réseau, l'état applicatif,
|
||||
//! les métriques et les layers Tower. Il est construit dans [`crate::core::App`]
|
||||
//! et démarré via [`HttpServer::run`].
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::middleware as axum_middleware;
|
||||
use axum::Router;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::broadcast;
|
||||
use tower_http::catch_panic::CatchPanicLayer;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
use crate::config::AppConfig;
|
||||
use crate::config::{AppConfig, NetworkConfig};
|
||||
use crate::core::AppState;
|
||||
use crate::http::OxRouter;
|
||||
use crate::routes;
|
||||
|
||||
pub async fn start(config: &AppConfig) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let addr = SocketAddr::new(
|
||||
std::net::IpAddr::V4(config.network.host),
|
||||
config.network.tcp_port,
|
||||
);
|
||||
use super::metrics::{self, HttpMetrics};
|
||||
use super::middleware::context_middleware;
|
||||
|
||||
let app: Router = routes::router();
|
||||
// ── Erreurs ───────────────────────────────────────────────────────────────────
|
||||
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
tracing::info!(%addr, "HTTP server listening");
|
||||
/// Erreurs pouvant survenir pendant l'opération du serveur HTTP.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum HttpServerError {
|
||||
#[error("failed to bind TCP listener to {addr}: {source}")]
|
||||
Bind {
|
||||
addr: SocketAddr,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
axum::serve(listener, app).await?;
|
||||
// ── Struct ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Serveur HTTP asynchrone basé sur axum.
|
||||
///
|
||||
/// - Injecte [`AppState`] via le middleware (les handlers l'obtiendront via
|
||||
/// `State<AppState>` quand ils seront implémentés ; `.with_state()` sera
|
||||
/// ajouté en conséquence).
|
||||
/// - Applique [`context_middleware`] (authentification JWT + contexte de
|
||||
/// requête) sur l'ensemble du router.
|
||||
/// - Empile les layers Tower : [`CatchPanicLayer`], [`CorsLayer`] (permissif),
|
||||
/// [`TraceLayer`].
|
||||
/// - Collecte des métriques via [`HttpMetrics`] et les reporte périodiquement.
|
||||
/// - Supporte un shutdown gracieux via un [`broadcast::Sender`].
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```no_run
|
||||
/// use oxspeak_server_lib::config::AppConfig;
|
||||
/// use oxspeak_server_lib::core::{App, AppState};
|
||||
/// use oxspeak_server_lib::http::server::HttpServer;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let config = AppConfig::load().unwrap();
|
||||
/// // AppState construit via App::build(config)
|
||||
/// }
|
||||
/// ```
|
||||
pub struct HttpServer {
|
||||
bind_addr: SocketAddr,
|
||||
app_state: AppState,
|
||||
metrics: Arc<HttpMetrics>,
|
||||
shutdown_rx: broadcast::Receiver<()>,
|
||||
}
|
||||
|
||||
impl HttpServer {
|
||||
/// Construit un [`HttpServer`] depuis la configuration et l'état applicatif.
|
||||
///
|
||||
/// Retourne le serveur et un [`broadcast::Sender`] pour déclencher le
|
||||
/// shutdown gracieux depuis l'extérieur.
|
||||
pub fn new(
|
||||
network_config: &NetworkConfig,
|
||||
app_state: AppState,
|
||||
) -> (Self, broadcast::Sender<()>) {
|
||||
let bind_addr = SocketAddr::new(network_config.host.into(), network_config.tcp_port);
|
||||
let metrics = HttpMetrics::new();
|
||||
let (shutdown_tx, shutdown_rx) = broadcast::channel(1);
|
||||
|
||||
(
|
||||
Self {
|
||||
bind_addr,
|
||||
app_state,
|
||||
metrics,
|
||||
shutdown_rx,
|
||||
},
|
||||
shutdown_tx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Retourne une référence aux métriques du serveur.
|
||||
pub fn metrics(&self) -> &Arc<HttpMetrics> {
|
||||
&self.metrics
|
||||
}
|
||||
|
||||
/// Bind le listener TCP et démarre la boucle de service.
|
||||
///
|
||||
/// La future se résout lorsqu'un signal de shutdown est reçu ou qu'une
|
||||
/// erreur I/O fatale survient.
|
||||
pub async fn run(mut self) -> Result<(), HttpServerError> {
|
||||
// Lance le reporter de métriques toutes les 30 secondes
|
||||
metrics::spawn_reporter(self.metrics.clone(), Duration::from_secs(30));
|
||||
|
||||
let metrics = self.metrics.clone();
|
||||
let app_state = self.app_state.clone();
|
||||
|
||||
// Construit le router avec state + middleware + layers Tower.
|
||||
//
|
||||
// Ordre d'application en axum : chaque `.layer()` enveloppe le service
|
||||
// courant — le dernier appel devient la couche la plus externe.
|
||||
//
|
||||
// CatchPanicLayer (outermost — intercepte tout panic en dessous)
|
||||
// └─ CorsLayer (répond aux preflight avant auth)
|
||||
// └─ TraceLayer (span tracing sur la durée totale)
|
||||
// └─ context_middleware (JWT + RequestContext + métriques)
|
||||
// └─ routes (handlers)
|
||||
let app: Router = routes::router()
|
||||
.with_state(app_state.clone())
|
||||
// Innermost : auth JWT, injection contexte, métriques
|
||||
.layer(axum_middleware::from_fn_with_state(
|
||||
app_state.clone(),
|
||||
move |state, req, next| {
|
||||
let metrics = metrics.clone();
|
||||
async move {
|
||||
metrics.inc_request();
|
||||
let started = std::time::Instant::now();
|
||||
let response = context_middleware(state, req, next).await;
|
||||
let latency = started.elapsed();
|
||||
metrics.record_response(response.status().as_u16(), latency);
|
||||
response
|
||||
}
|
||||
},
|
||||
))
|
||||
// Spans tracing par requête (method, uri, status, latency)
|
||||
.layer(TraceLayer::new_for_http())
|
||||
// CORS permissif (à affiner en production)
|
||||
.layer(CorsLayer::permissive())
|
||||
// Outermost : intercepte les panics et retourne une 500 propre
|
||||
.layer(CatchPanicLayer::new());
|
||||
|
||||
let listener =
|
||||
TcpListener::bind(self.bind_addr)
|
||||
.await
|
||||
.map_err(|source| HttpServerError::Bind {
|
||||
addr: self.bind_addr,
|
||||
source,
|
||||
})?;
|
||||
|
||||
tracing::info!(addr = %self.bind_addr, "HTTP server listening");
|
||||
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(async move {
|
||||
let _ = self.shutdown_rx.recv().await;
|
||||
tracing::info!("HTTP server shutting down");
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
use axum::{
|
||||
extract::{FromRequest, Request},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ValidationErrorResponse {
|
||||
pub errors: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
/// Extracteur qui valide le JSON entrant à l'aide de la crate `validator`.
|
||||
/// Si la validation échoue (JSON malformé ou règles métier violées),
|
||||
/// il retourne une erreur 400 avec le détail par champ.
|
||||
pub struct ValidatedJson<T>(pub T);
|
||||
|
||||
impl<S, T> FromRequest<S> for ValidatedJson<T>
|
||||
where
|
||||
S: Send + Sync,
|
||||
T: DeserializeOwned + Validate + 'static,
|
||||
{
|
||||
type Rejection = Response;
|
||||
|
||||
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||
// 1. Tente d'extraire le JSON.
|
||||
// Si le JSON est syntaxiquement invalide, on retourne l'erreur d'Axum directement.
|
||||
let Json(value) = Json::<T>::from_request(req, state)
|
||||
.await
|
||||
.map_err(|rejection| rejection.into_response())?;
|
||||
|
||||
// 2. Exécute la validation définie par #[derive(Validate)] sur le DTO.
|
||||
value.validate().map_err(|errors| {
|
||||
let mut error_map = HashMap::new();
|
||||
|
||||
for (field, field_errors) in errors.field_errors() {
|
||||
let messages: Vec<String> = field_errors
|
||||
.iter()
|
||||
.map(|e| {
|
||||
e.message
|
||||
.as_ref()
|
||||
.map(|m| m.to_string())
|
||||
.unwrap_or_else(|| e.code.to_string())
|
||||
})
|
||||
.collect();
|
||||
error_map.insert(field.to_string(), messages);
|
||||
}
|
||||
|
||||
// Retourne un format JSON structuré : {"errors": {"field": ["msg"]}}
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ValidationErrorResponse { errors: error_map }),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
Ok(ValidatedJson(value))
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -8,4 +8,5 @@ pub mod permissions;
|
||||
pub mod repositories;
|
||||
pub mod routes;
|
||||
pub mod udp;
|
||||
pub mod utils;
|
||||
|
||||
pub mod auth;
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// 3. Build & Run
|
||||
let app = App::build(config).await?;
|
||||
app.run().await;
|
||||
app.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ pub struct Model {
|
||||
pub password: Option<String>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub is_default: bool,
|
||||
|
||||
/// Permissions serveur par défaut (stockées en i64, lues en u64)
|
||||
pub default_server_permissions: i64,
|
||||
@@ -55,6 +56,7 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
let default_perm = PermissionSet::DEFAULT;
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
is_default: Set(false),
|
||||
default_server_permissions: Set(default_perm.server.bits() as i64),
|
||||
default_channel_permissions: Set(default_perm.channel.bits() as i64),
|
||||
default_voice_permissions: Set(default_perm.voice.bits() as i64),
|
||||
|
||||
@@ -1,45 +1,51 @@
|
||||
use std::sync::Arc;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter,
|
||||
};
|
||||
use crate::models::category;
|
||||
use crate::repositories::RepositoryContext;
|
||||
use crate::repositories::{AnyResult, RepositoryContext};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CategoryRepository {
|
||||
pub context: Arc<RepositoryContext>
|
||||
pub context: Arc<RepositoryContext>,
|
||||
}
|
||||
|
||||
impl CategoryRepository {
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<category::Model>, DbErr> {
|
||||
category::Entity::find_by_id(id).one(&self.context.db).await
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> AnyResult<Option<category::Model>> {
|
||||
Ok(category::Entity::find_by_id(id)
|
||||
.one(&self.context.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_all(&self) -> Result<Vec<category::Model>, DbErr> {
|
||||
category::Entity::find().all(&self.context.db).await
|
||||
pub async fn get_all(&self) -> AnyResult<Vec<category::Model>> {
|
||||
Ok(category::Entity::find().all(&self.context.db).await?)
|
||||
}
|
||||
|
||||
pub async fn get_by_server_id(&self, server_id: uuid::Uuid) -> Result<Vec<category::Model>, DbErr> {
|
||||
category::Entity::find()
|
||||
pub async fn get_by_server_id(&self, server_id: uuid::Uuid) -> AnyResult<Vec<category::Model>> {
|
||||
Ok(category::Entity::find()
|
||||
.filter(category::Column::ServerId.eq(server_id))
|
||||
.all(&self.context.db)
|
||||
.await
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: category::ActiveModel) -> Result<category::Model, DbErr> {
|
||||
pub async fn update(&self, active: category::ActiveModel) -> AnyResult<category::Model> {
|
||||
let category = active.update(&self.context.db).await?;
|
||||
self.context.events.emit("Category_updated", category.clone());
|
||||
self.context
|
||||
.events
|
||||
.emit("Category_updated", category.clone());
|
||||
Ok(category)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: category::ActiveModel) -> Result<category::Model, DbErr> {
|
||||
pub async fn create(&self, active: category::ActiveModel) -> AnyResult<category::Model> {
|
||||
let category = active.insert(&self.context.db).await?;
|
||||
self.context.events.emit("Category_created", category.clone());
|
||||
self.context
|
||||
.events
|
||||
.emit("Category_created", category.clone());
|
||||
Ok(category)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||
category::Entity::delete_by_id(id).exec(&self.context.db).await?;
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> AnyResult<()> {
|
||||
category::Entity::delete_by_id(id)
|
||||
.exec(&self.context.db)
|
||||
.await?;
|
||||
self.context.events.emit("Category_deleted", id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
use crate::models::channel;
|
||||
use crate::repositories::RepositoryContext;
|
||||
use sea_orm::{ActiveModelTrait, DbErr, EntityTrait};
|
||||
use crate::repositories::{AnyResult, RepositoryContext};
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChannelRepository {
|
||||
pub context: Arc<RepositoryContext>,
|
||||
}
|
||||
|
||||
impl ChannelRepository {
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<channel::Model>, DbErr> {
|
||||
channel::Entity::find_by_id(id).one(&self.context.db).await
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> AnyResult<Option<channel::Model>> {
|
||||
Ok(channel::Entity::find_by_id(id)
|
||||
.one(&self.context.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: channel::ActiveModel) -> Result<channel::Model, DbErr> {
|
||||
pub async fn update(&self, active: channel::ActiveModel) -> AnyResult<channel::Model> {
|
||||
let channel = active.update(&self.context.db).await?;
|
||||
self.context.events.emit("channel_updated", channel.clone());
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: channel::ActiveModel) -> Result<channel::Model, DbErr> {
|
||||
pub async fn create(&self, active: channel::ActiveModel) -> AnyResult<channel::Model> {
|
||||
let channel = active.insert(&self.context.db).await?;
|
||||
self.context.events.emit("channel_created", channel.clone());
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> AnyResult<()> {
|
||||
channel::Entity::delete_by_id(id)
|
||||
.exec(&self.context.db)
|
||||
.await?;
|
||||
|
||||
+11
-11
@@ -1,39 +1,39 @@
|
||||
use crate::models::group;
|
||||
use crate::repositories::RepositoryContext;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter};
|
||||
use crate::repositories::{AnyResult, RepositoryContext};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GroupRepository {
|
||||
pub context: Arc<RepositoryContext>,
|
||||
}
|
||||
|
||||
impl GroupRepository {
|
||||
pub async fn get_all_by_server(&self, server_id: Uuid) -> Result<Vec<group::Model>, DbErr> {
|
||||
group::Entity::find()
|
||||
pub async fn get_all_by_server(&self, server_id: Uuid) -> AnyResult<Vec<group::Model>> {
|
||||
Ok(group::Entity::find()
|
||||
.filter(group::Column::ServerId.eq(server_id))
|
||||
.all(&self.context.db)
|
||||
.await
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_by_id(&self, id: Uuid) -> Result<Option<group::Model>, DbErr> {
|
||||
group::Entity::find_by_id(id).one(&self.context.db).await
|
||||
pub async fn get_by_id(&self, id: Uuid) -> AnyResult<Option<group::Model>> {
|
||||
Ok(group::Entity::find_by_id(id).one(&self.context.db).await?)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: group::ActiveModel) -> Result<group::Model, DbErr> {
|
||||
pub async fn create(&self, active: group::ActiveModel) -> AnyResult<group::Model> {
|
||||
let group = active.insert(&self.context.db).await?;
|
||||
self.context.events.emit("group_created", group.clone());
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: group::ActiveModel) -> Result<group::Model, DbErr> {
|
||||
pub async fn update(&self, active: group::ActiveModel) -> AnyResult<group::Model> {
|
||||
let group = active.update(&self.context.db).await?;
|
||||
self.context.events.emit("group_updated", group.clone());
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: Uuid) -> Result<bool, DbErr> {
|
||||
pub async fn delete(&self, id: Uuid) -> AnyResult<bool> {
|
||||
let res = group::Entity::delete_by_id(id)
|
||||
.exec(&self.context.db)
|
||||
.await?;
|
||||
|
||||
+15
-11
@@ -1,32 +1,36 @@
|
||||
use std::sync::Arc;
|
||||
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait};
|
||||
use crate::models::message;
|
||||
use crate::repositories::RepositoryContext;
|
||||
use crate::repositories::{AnyResult, RepositoryContext};
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MessageRepository {
|
||||
pub context: Arc<RepositoryContext>
|
||||
pub context: Arc<RepositoryContext>,
|
||||
}
|
||||
|
||||
impl MessageRepository {
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<message::Model>, DbErr> {
|
||||
message::Entity::find_by_id(id).one(&self.context.db).await
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> AnyResult<Option<message::Model>> {
|
||||
Ok(message::Entity::find_by_id(id)
|
||||
.one(&self.context.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: message::ActiveModel) -> Result<message::Model, DbErr> {
|
||||
pub async fn update(&self, active: message::ActiveModel) -> AnyResult<message::Model> {
|
||||
let message = active.update(&self.context.db).await?;
|
||||
self.context.events.emit("message_updated", message.clone());
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: message::ActiveModel) -> Result<message::Model, DbErr> {
|
||||
pub async fn create(&self, active: message::ActiveModel) -> AnyResult<message::Model> {
|
||||
let message = active.insert(&self.context.db).await?;
|
||||
self.context.events.emit("message_created", message.clone());
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||
message::Entity::delete_by_id(id).exec(&self.context.db).await?;
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> AnyResult<()> {
|
||||
message::Entity::delete_by_id(id)
|
||||
.exec(&self.context.db)
|
||||
.await?;
|
||||
self.context.events.emit("message_deleted", id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub type AnyResult<T> = anyhow::Result<T>;
|
||||
|
||||
use crate::repositories::category::CategoryRepository;
|
||||
use crate::repositories::channel::ChannelRepository;
|
||||
use crate::repositories::group::GroupRepository;
|
||||
@@ -16,13 +18,13 @@ mod server;
|
||||
pub mod types;
|
||||
mod user;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RepositoryContext {
|
||||
db: DatabaseConnection,
|
||||
events: Arc<EventBus>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Repositories {
|
||||
pub server: ServerRepository,
|
||||
pub category: CategoryRepository,
|
||||
@@ -33,11 +35,8 @@ pub struct Repositories {
|
||||
}
|
||||
|
||||
impl Repositories {
|
||||
pub fn new(db: &DatabaseConnection, events: Arc<EventBus>) -> Self {
|
||||
let context = Arc::new(RepositoryContext {
|
||||
db: db.clone(),
|
||||
events,
|
||||
});
|
||||
pub fn new(db: DatabaseConnection, events: Arc<EventBus>) -> Self {
|
||||
let context = Arc::new(RepositoryContext { db, events });
|
||||
|
||||
Self {
|
||||
server: ServerRepository {
|
||||
|
||||
+53
-12
@@ -1,32 +1,40 @@
|
||||
use super::types::{ServerExplorerItem, ServerTree};
|
||||
use super::RepositoryContext;
|
||||
use crate::models::{category, channel, group, server};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set};
|
||||
use super::{AnyResult, RepositoryContext};
|
||||
use crate::models::{category, channel, group, server, server_user};
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ServerRepository {
|
||||
pub context: Arc<RepositoryContext>,
|
||||
}
|
||||
|
||||
impl ServerRepository {
|
||||
pub async fn get_all(&self) -> Result<Vec<server::Model>, DbErr> {
|
||||
server::Entity::find().all(&self.context.db).await
|
||||
pub async fn get_all(&self) -> AnyResult<Vec<server::Model>> {
|
||||
Ok(server::Entity::find().all(&self.context.db).await?)
|
||||
}
|
||||
|
||||
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 get_by_id(&self, id: Uuid) -> AnyResult<Option<server::Model>> {
|
||||
Ok(server::Entity::find_by_id(id).one(&self.context.db).await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: server::ActiveModel) -> Result<server::Model, DbErr> {
|
||||
pub async fn get_default(&self) -> AnyResult<Option<server::Model>> {
|
||||
Ok(server::Entity::find()
|
||||
.filter(server::Column::IsDefault.eq(true))
|
||||
.one(&self.context.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: server::ActiveModel) -> AnyResult<server::Model> {
|
||||
let server = active.update(&self.context.db).await?;
|
||||
self.context.events.emit("server_updated", server.clone());
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: server::ActiveModel) -> Result<server::Model, DbErr> {
|
||||
pub async fn create(&self, active: server::ActiveModel) -> AnyResult<server::Model> {
|
||||
let server = active.insert(&self.context.db).await?;
|
||||
|
||||
// Créer le groupe par défaut pour le serveur
|
||||
@@ -42,18 +50,51 @@ impl ServerRepository {
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<bool, DbErr> {
|
||||
pub async fn create_with_args(
|
||||
&self,
|
||||
name: String,
|
||||
is_default: bool,
|
||||
) -> AnyResult<server::Model> {
|
||||
let active = server::ActiveModel {
|
||||
name: Set(name),
|
||||
is_default: Set(is_default),
|
||||
..Default::default()
|
||||
};
|
||||
self.create(active).await
|
||||
}
|
||||
|
||||
pub async fn add_user(&self, server_id: Uuid, user_id: Uuid) -> AnyResult<bool> {
|
||||
let res = server_user::ActiveModel {
|
||||
server_id: Set(server_id),
|
||||
user_id: Set(user_id),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&self.context.db)
|
||||
.await?;
|
||||
|
||||
self.context
|
||||
.events
|
||||
.emit("server_user_created", (server_id, user_id));
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: Uuid) -> AnyResult<bool> {
|
||||
let res = server::Entity::delete_by_id(id)
|
||||
.exec(&self.context.db)
|
||||
.await?;
|
||||
self.context.events.emit("server_deleted", id);
|
||||
Ok(res.rows_affected > 0)
|
||||
}
|
||||
|
||||
pub async fn count(&self) -> AnyResult<usize> {
|
||||
let res = server::Entity::find().count(&self.context.db).await?;
|
||||
Ok(res as usize)
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
impl ServerRepository {
|
||||
pub async fn get_tree(&self, server_id: Uuid) -> Result<ServerTree, DbErr> {
|
||||
pub async fn get_tree(&self, server_id: Uuid) -> AnyResult<ServerTree> {
|
||||
// 1. Récupération des catégories avec leurs channels
|
||||
let categories_with_channels = category::Entity::find()
|
||||
.filter(category::Column::ServerId.eq(server_id))
|
||||
|
||||
+32
-29
@@ -1,75 +1,70 @@
|
||||
use crate::auth::password;
|
||||
use crate::models::user;
|
||||
use crate::repositories::RepositoryContext;
|
||||
use crate::utils::password;
|
||||
use crate::repositories::{AnyResult, RepositoryContext};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, IntoActiveModel, PaginatorTrait,
|
||||
QueryFilter, Set,
|
||||
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter, Set,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UserRepository {
|
||||
pub context: Arc<RepositoryContext>,
|
||||
}
|
||||
|
||||
impl UserRepository {
|
||||
pub async fn get_all(&self) -> Result<Vec<user::Model>, DbErr> {
|
||||
user::Entity::find().all(&self.context.db).await
|
||||
pub async fn get_all(&self) -> AnyResult<Vec<user::Model>> {
|
||||
Ok(user::Entity::find().all(&self.context.db).await?)
|
||||
}
|
||||
|
||||
pub async fn count(&self) -> Result<u64, DbErr> {
|
||||
user::Entity::find().count(&self.context.db).await
|
||||
pub async fn count(&self) -> AnyResult<u64> {
|
||||
Ok(user::Entity::find().count(&self.context.db).await?)
|
||||
}
|
||||
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<user::Model>, DbErr> {
|
||||
user::Entity::find_by_id(id).one(&self.context.db).await
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> AnyResult<Option<user::Model>> {
|
||||
Ok(user::Entity::find_by_id(id).one(&self.context.db).await?)
|
||||
}
|
||||
|
||||
pub async fn get_by_username(&self, username: String) -> Result<Option<user::Model>, DbErr> {
|
||||
user::Entity::find()
|
||||
pub async fn get_by_username(&self, username: String) -> AnyResult<Option<user::Model>> {
|
||||
Ok(user::Entity::find()
|
||||
.filter(user::Column::Username.eq(username))
|
||||
.one(&self.context.db)
|
||||
.await
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn check_password(
|
||||
&self,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> Result<user::Model, DbErr> {
|
||||
let user = self.get_by_username(username).await?;
|
||||
pub async fn check_password(&self, username: &str, password: &str) -> AnyResult<user::Model> {
|
||||
let user = self.get_by_username(username.to_string()).await?;
|
||||
if let Some(user) = user {
|
||||
let password_ok = password::verify_password(password.as_str(), user.password.as_str())
|
||||
.map_err(|_| DbErr::Custom("Password hashing failed".to_string()))?;
|
||||
let password_ok = password::verify_password(password, user.password.as_str())
|
||||
.map_err(|e| anyhow::anyhow!("Password hashing failed: {}", e))?;
|
||||
|
||||
if password_ok {
|
||||
return Ok(user);
|
||||
}
|
||||
}
|
||||
Err(DbErr::Custom("Invalid username or password".to_string()))
|
||||
Err(anyhow::anyhow!("Invalid username or password"))
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: user::ActiveModel) -> Result<user::Model, DbErr> {
|
||||
pub async fn update(&self, active: user::ActiveModel) -> AnyResult<user::Model> {
|
||||
let user = active.update(&self.context.db).await?;
|
||||
self.context.events.emit("user_updated", user.clone());
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: user::ActiveModel) -> Result<user::Model, DbErr> {
|
||||
pub async fn create(&self, active: user::ActiveModel) -> AnyResult<user::Model> {
|
||||
let user = active.insert(&self.context.db).await?;
|
||||
self.context.events.emit("user_created", user.clone());
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn set_password(&self, id: uuid::Uuid, password: String) -> Result<(), DbErr> {
|
||||
pub async fn set_password(&self, id: uuid::Uuid, password: String) -> AnyResult<()> {
|
||||
let user = self
|
||||
.get_by_id(id)
|
||||
.await?
|
||||
.ok_or_else(|| DbErr::Custom("User not found".to_string()))?;
|
||||
.ok_or_else(|| anyhow::anyhow!("User not found"))?;
|
||||
|
||||
let mut active = user.into_active_model();
|
||||
let password = password::hash_password(&password)
|
||||
.map_err(|_| DbErr::Custom("Password hashing failed".to_string()))?;
|
||||
.map_err(|e| anyhow::anyhow!("Password hashing failed: {}", e))?;
|
||||
active.password = Set(password);
|
||||
|
||||
let user = self.update(active).await?;
|
||||
@@ -78,11 +73,19 @@ impl UserRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> AnyResult<()> {
|
||||
user::Entity::delete_by_id(id)
|
||||
.exec(&self.context.db)
|
||||
.await?;
|
||||
self.context.events.emit("user_deleted", id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn username_exists(&self, username: &str) -> AnyResult<bool> {
|
||||
Ok(user::Entity::find()
|
||||
.filter(user::Column::Username.eq(username))
|
||||
.count(&self.context.db)
|
||||
.await?
|
||||
> 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct LoginRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct LoginResponse {
|
||||
pub token: String,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct CheckResponse {
|
||||
pub authenticated: bool,
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use super::dto::{CheckResponse, LoginRequest, LoginResponse};
|
||||
use crate::auth::token::create_jwt;
|
||||
use crate::core::AppState;
|
||||
use crate::http::context::CurrentUser;
|
||||
use crate::http::error::HTTPError;
|
||||
use axum::extract::State;
|
||||
use axum::Json;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/login",
|
||||
responses(
|
||||
(status = 200, description = "Login successful", body = LoginResponse),
|
||||
(status = 401, description = "Unauthorized")
|
||||
)
|
||||
)]
|
||||
pub async fn login_user_pw(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<LoginRequest>,
|
||||
) -> Result<Json<LoginResponse>, HTTPError> {
|
||||
let user = state
|
||||
.repositories
|
||||
.user
|
||||
.check_password(&payload.username, &payload.password)
|
||||
.await
|
||||
.map_err(|_| HTTPError::Unauthorized)?;
|
||||
|
||||
let token = create_jwt(
|
||||
user.id,
|
||||
&user.username,
|
||||
&state.config.jwt.secret,
|
||||
state.config.jwt.duration,
|
||||
)
|
||||
.map_err(|_| HTTPError::InternalServerError("Failed to create JWT token".to_string()))?;
|
||||
|
||||
Ok(Json(LoginResponse {
|
||||
username: user.username,
|
||||
token,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/check",
|
||||
responses(
|
||||
(status = 200, description = "Login successful", body = LoginResponse),
|
||||
(status = 401, description = "Unauthorized")
|
||||
)
|
||||
)]
|
||||
pub async fn check(
|
||||
State(state): State<AppState>,
|
||||
user: CurrentUser,
|
||||
) -> Result<Json<CheckResponse>, HTTPError> {
|
||||
Ok(Json(CheckResponse {
|
||||
authenticated: true,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod domain;
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
pub mod mapper;
|
||||
pub mod routes;
|
||||
pub mod service;
|
||||
@@ -0,0 +1,16 @@
|
||||
use crate::http::OxRouter;
|
||||
use crate::routes::auth::handlers;
|
||||
use axum::routing::post;
|
||||
use axum::Router;
|
||||
|
||||
pub fn router() -> OxRouter {
|
||||
Router::new().route("/login", post(handlers::login_user_pw))
|
||||
|
||||
// .route("/categorys", get(handlers::get_all).post(handlers::create))
|
||||
// .route(
|
||||
// "/categorys/:id",
|
||||
// get(handlers::get_by_id)
|
||||
// .put(handlers::update)
|
||||
// .delete(handlers::delete),
|
||||
// )
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
use serde::Deserialize;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Deserialize, Validate, ToSchema)]
|
||||
pub struct JoinRequest {
|
||||
#[validate(length(min = 3, message = "Username must be at least 3 characters long"))]
|
||||
pub username: String,
|
||||
#[validate(length(min = 8, message = "Password must be at least 8 characters long"))]
|
||||
pub password: String,
|
||||
#[validate(must_match(other = "password", message = "Passwords do not match"))]
|
||||
pub password_valid: String,
|
||||
pub super_admin_token: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use crate::core::AppState;
|
||||
use crate::http::error::HTTPError;
|
||||
use crate::http::validation::ValidatedJson;
|
||||
use crate::routes::core::dto::JoinRequest;
|
||||
use crate::routes::core::mapper::join_request_to_user_am;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
|
||||
pub async fn join(
|
||||
State(state): State<AppState>,
|
||||
ValidatedJson(payload): ValidatedJson<JoinRequest>,
|
||||
) -> Result<impl IntoResponse, HTTPError> {
|
||||
let user_exists = state
|
||||
.repositories
|
||||
.user
|
||||
.username_exists(&payload.username)
|
||||
.await?;
|
||||
|
||||
if user_exists {
|
||||
return Err(HTTPError::validation_error(
|
||||
"username",
|
||||
"Username already exists",
|
||||
));
|
||||
};
|
||||
|
||||
let user_am = join_request_to_user_am(payload)?;
|
||||
let user = state.repositories.user.create(user_am).await?;
|
||||
state
|
||||
.repositories
|
||||
.server
|
||||
.add_user(state.default_server.id, user.id)
|
||||
.await?;
|
||||
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use crate::auth::password::hash_password;
|
||||
use crate::models::user;
|
||||
use crate::routes::core::dto::JoinRequest;
|
||||
use anyhow::Result as AnyResult;
|
||||
use sea_orm::Set;
|
||||
|
||||
pub fn join_request_to_user_am(payload: JoinRequest) -> AnyResult<user::ActiveModel> {
|
||||
Ok(user::ActiveModel {
|
||||
id: Default::default(),
|
||||
username: Set(payload.username),
|
||||
password: Set(hash_password(&payload.password)?),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod domain;
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
pub mod mapper;
|
||||
pub mod routes;
|
||||
pub mod service;
|
||||
@@ -0,0 +1,7 @@
|
||||
use super::handlers;
|
||||
use crate::http::OxRouter;
|
||||
use axum::{routing::get, Router};
|
||||
|
||||
pub fn router() -> OxRouter {
|
||||
Router::new().route("/join", get(handlers::join))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
+12
-9
@@ -1,20 +1,23 @@
|
||||
use crate::http::OxRouter;
|
||||
use axum::Router;
|
||||
|
||||
pub mod attachment;
|
||||
pub mod auth;
|
||||
pub mod category;
|
||||
pub mod channel;
|
||||
pub mod core;
|
||||
pub mod group;
|
||||
pub mod message;
|
||||
pub mod server;
|
||||
pub mod user;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.merge(user::routes::router())
|
||||
.merge(server::routes::router())
|
||||
.merge(channel::routes::router())
|
||||
.merge(message::routes::router())
|
||||
.merge(group::routes::router())
|
||||
.merge(category::routes::router())
|
||||
.merge(attachment::routes::router())
|
||||
pub fn router() -> OxRouter {
|
||||
Router::new().merge(auth::routes::router())
|
||||
// .merge(user::routes::router())
|
||||
// .merge(server::routes::router())
|
||||
// .merge(channel::routes::router())
|
||||
// .merge(message::routes::router())
|
||||
// .merge(group::routes::router())
|
||||
// .merge(category::routes::router())
|
||||
// .merge(attachment::routes::router())
|
||||
}
|
||||
|
||||
+2
-5
@@ -67,14 +67,11 @@ pub struct UdpServer {
|
||||
impl UdpServer {
|
||||
/// Crée un nouveau [`UdpServer`] depuis la configuration réseau globale.
|
||||
///
|
||||
/// `metrics` est un [`Arc`] vers le jeu de métriques à alimenter.
|
||||
/// Conservez-en un clone si vous souhaitez l'observer depuis l'extérieur
|
||||
/// (reporter, API de santé, etc.).
|
||||
///
|
||||
/// Retourne le serveur et un [`broadcast::Sender`] pour déclencher le
|
||||
/// shutdown gracieux.
|
||||
pub fn new(network: NetworkConfig, metrics: Arc<UdpMetrics>) -> (Self, broadcast::Sender<()>) {
|
||||
pub fn new(network: &NetworkConfig) -> (Self, broadcast::Sender<()>) {
|
||||
let bind_addr = SocketAddr::new(network.host.into(), network.udp_port);
|
||||
let metrics = UdpMetrics::new();
|
||||
let (shutdown_tx, shutdown_rx) = broadcast::channel(1);
|
||||
(
|
||||
Self {
|
||||
|
||||
Reference in New Issue
Block a user