From f9218e0927aebcf98df470fdb5a8f36263286ed7 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:16:21 +0800 Subject: [PATCH 01/11] Add support for environment file generation in EnvFile struct - Introduced new methods for writing environment files in YAML and DotEnv formats. - Updated EnvFile struct to include a buffer for storing key-value pairs. - Modified write_env_files function to create and write to environment files based on configuration. - Added tests for environment file writing functionality. --- .gitignore | 3 + apps/container/Cargo.toml | 1 + apps/container/src/env.rs | 153 +++++++++++++++++++++++++++++++++---- apps/container/src/util.rs | 19 ++--- 4 files changed, 154 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 87af617..146633d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ target #.idea/ # generated environment variables file +.env .env.generated + +generated-config.yaml diff --git a/apps/container/Cargo.toml b/apps/container/Cargo.toml index 6b53abc..bc502e1 100644 --- a/apps/container/Cargo.toml +++ b/apps/container/Cargo.toml @@ -15,3 +15,4 @@ tokio = { version = "1.47.0", features = ["full"] } url = "2.5.7" clap = { version = "4.5.48", features = ["derive", "env"] } path-clean = "1.0.1" +serde_json = "1.0.145" diff --git a/apps/container/src/env.rs b/apps/container/src/env.rs index ec1822c..162883f 100644 --- a/apps/container/src/env.rs +++ b/apps/container/src/env.rs @@ -13,24 +13,151 @@ pub struct EnvFile { pub file_type: EnvFileType, pub db_type: DBType, pub db_url: String, + // + buffer: serde_json::Value, } impl EnvFile { - pub fn write(self, path: impl AsRef) { - let path_ref = path.as_ref(); - println!("Config file path: {}", path_ref.display()); - let mut config_file = - std::fs::File::create(path_ref).expect("Failed to create config file"); - // - self._write_line(&mut config_file, "DB_TYPE", &self.db_type.to_string()); - self._write_line(&mut config_file, "DATABASE_URL", &self.db_url.to_string()) + pub fn new(file_type: EnvFileType, db_type: DBType, db_url: String) -> Self { + let mut env_file = EnvFile { + file_type, + db_type, + db_url, + buffer: serde_json::Value::Object(serde_json::Map::new()), + }; + + env_file._write_line_buffer("DATABASE__TYPE", &env_file.db_type.to_string()); + env_file._write_line_buffer("DATABASE__URL", &env_file.db_url.to_string()); + + env_file } - fn _write_line(&self, file: &mut std::fs::File, key: &str, value: &str) { - match self.file_type { - EnvFileType::DotEnv => writeln!(file, "{}={}", key, value), - EnvFileType::Yaml => writeln!(file, "{}: \"{}\"", key, value), + pub fn write(&mut self, stream: &mut dyn Write, with_prefix: bool) { + self._write_buffer(stream, with_prefix); + } + + fn key_into_buffer_key(&self, key: &str) -> Vec { + key.split("__").map(String::from).collect() + } + + fn _write_line_buffer(&mut self, key: &str, value: &str) { + let buffer_key = self.key_into_buffer_key(key); + let mut current = &mut self.buffer; + for k in &buffer_key[0..(buffer_key.len() - 1)] { + if current.get(k).is_none() { + current[k] = serde_json::Value::Object(serde_json::Map::new()); + } + current = &mut current[k]; } - .expect("Failed to write to config file"); + current[buffer_key.last().unwrap()] = serde_json::Value::String(value.to_string()); + } + + fn _write_buffer(&self, file: &mut dyn Write, with_prefix: bool) { + match self.file_type { + EnvFileType::DotEnv => self._write_buffer_env(file, with_prefix), + EnvFileType::Yaml => self._write_buffer_yaml(file), + } + } + + fn _write_buffer_env(&self, file: &mut dyn Write, with_prefix: bool) { + fn _write_buffer_env_layer( + file: &mut dyn Write, + buffer: &serde_json::Value, + prefix: String, + with_root_prefix: bool, + ) { + if let serde_json::Value::Object(map) = buffer { + for (key, value) in map { + let current_key = if prefix.is_empty() { + if with_root_prefix { + format!("YANPM__{}", key) + } else { + key.to_string() + } + } else { + format!("{}__{}", prefix, key) + }; + match value { + serde_json::Value::Object(_) => { + _write_buffer_env_layer(file, value, current_key, with_root_prefix); + } + _ => { + writeln!(file, "{}={}", current_key, value).unwrap(); + } + } + } + } + } + + _write_buffer_env_layer(file, &self.buffer, String::new(), with_prefix); + } + + fn _write_buffer_yaml(&self, file: &mut dyn Write) { + let mut layer = 0; + fn _write_buffer_yaml_layer( + file: &mut dyn Write, + buffer: &serde_json::Value, + layer: &mut usize, + ) { + if let serde_json::Value::Object(map) = buffer { + for (key, value) in map { + let indent = " ".repeat(*layer); + match value { + serde_json::Value::Object(_) => { + writeln!(file, "{}{}:", indent, key).unwrap(); + *layer += 1; + _write_buffer_yaml_layer(file, value, layer); + *layer -= 1; + } + _ => { + writeln!(file, "{}{}: {}", indent, key, value).unwrap(); + } + } + } + } + } + + _write_buffer_yaml_layer(file, &self.buffer, &mut layer); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_env_file_write_yaml() { + let mut env_file_nested = EnvFile::new( + EnvFileType::Yaml, + DBType::SQLite, + "mysql://user:pass@localhost/db".to_string(), + ); + + let mut output_stream = Vec::new(); + env_file_nested.write(&mut output_stream, false); + let output_string = String::from_utf8(output_stream).unwrap(); + let expected_output = "\ +DATABASE: + TYPE: \"SQLite\" + URL: \"mysql://user:pass@localhost/db\" +"; + assert_eq!(output_string, expected_output); + } + + #[test] + fn test_env_file_write_env() { + let mut env_file_nested = EnvFile::new( + EnvFileType::DotEnv, + DBType::PostgreSQL, + "postgres://user:pass@localhost/db".to_string(), + ); + let mut output_stream = Vec::new(); + env_file_nested.write(&mut output_stream, true); + let output_string = String::from_utf8(output_stream).unwrap(); + let expected_output = "\ +YANPM__DATABASE__TYPE=\"PostgreSQL\" +YANPM__DATABASE__URL=\"postgres://user:pass@localhost/db\" +"; + assert_eq!(output_string, expected_output); } } diff --git a/apps/container/src/util.rs b/apps/container/src/util.rs index 9783ae1..d513904 100644 --- a/apps/container/src/util.rs +++ b/apps/container/src/util.rs @@ -29,17 +29,18 @@ pub fn write_env_files(db_config: &DBConfigInfoType) { DBConfigInfoType::PreExisting(config) => (config.db_type.clone(), config.url.clone()), }; - let api_env_file = EnvFile { - file_type: env::EnvFileType::Yaml, - db_type, - db_url, - }; + let mut api_env = EnvFile::new(env::EnvFileType::Yaml, db_type, db_url); + let mut db_env = api_env.clone(); + db_env.file_type = env::EnvFileType::DotEnv; - let mut db_env_file = api_env_file.clone(); - db_env_file.file_type = env::EnvFileType::DotEnv; + let mut api_file = + std::fs::File::create(&api_config_path_absolute).expect("Failed to create API config file"); - api_env_file.write(&api_config_path_absolute); - db_env_file.write(&db_config_path_absolute); + let mut db_file = + std::fs::File::create(&db_config_path_absolute).expect("Failed to create DB config file"); + + api_env.write(&mut api_file, true); + db_env.write(&mut db_file, false); } pub async fn stop_container( -- 2.49.1 From 56c1161e9790b5e7d71c2032e9e58877c5a5dab6 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:17:03 +0800 Subject: [PATCH 02/11] Change database log level to debug --- public/database/Cargo.toml | 1 + public/database/src/lib.rs | 4 +++- public/migration/Cargo.toml | 1 + public/migration/src/lib.rs | 13 +++++++++++-- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/public/database/Cargo.toml b/public/database/Cargo.toml index 86c0b16..c180e41 100644 --- a/public/database/Cargo.toml +++ b/public/database/Cargo.toml @@ -14,6 +14,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.47.0", features = ["full"] } sea-orm = { version = "2.0.0-rc", features = [ "sqlx-postgres", "sqlx-mysql", "sqlx-sqlite", "runtime-tokio-rustls", "macros", "mock", "with-chrono", "with-json", "with-uuid", "sqlite-use-returning-for-3_35", "mariadb-use-returning" ] } +log = "0.4.28" [lints] workspace = true diff --git a/public/database/src/lib.rs b/public/database/src/lib.rs index ebfa1f8..7985e66 100644 --- a/public/database/src/lib.rs +++ b/public/database/src/lib.rs @@ -1,3 +1,4 @@ +use log::LevelFilter; pub use sea_orm::ConnectOptions; pub mod generated; @@ -13,7 +14,8 @@ pub async fn get_connection( .connect_timeout(std::time::Duration::from_secs(8)) .idle_timeout(std::time::Duration::from_secs(8)) .test_before_acquire(true) - .sqlx_logging(true); + .sqlx_logging(true) + .sqlx_logging_level(LevelFilter::Debug); if let Some(option_fn) = option_fn { option_fn(&mut opt); diff --git a/public/migration/Cargo.toml b/public/migration/Cargo.toml index 8241205..fa42db8 100644 --- a/public/migration/Cargo.toml +++ b/public/migration/Cargo.toml @@ -12,6 +12,7 @@ path = "src/lib.rs" [dependencies] tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } sea-orm-cli = { version = "2.0.0-rc", features = ["sqlx-postgres", "sqlx-mysql", "sqlx-sqlite", "runtime-tokio"] } +log = "0.4.28" [dependencies.sea-orm-migration] version = "2.0.0-rc" diff --git a/public/migration/src/lib.rs b/public/migration/src/lib.rs index 7488856..1be42ae 100644 --- a/public/migration/src/lib.rs +++ b/public/migration/src/lib.rs @@ -2,7 +2,7 @@ pub use sea_orm_migration::prelude::*; mod migrations; use migrations::*; -use sea_orm_migration::sea_orm::Database; +use sea_orm_migration::sea_orm::{ConnectOptions, Database}; pub struct Migrator; @@ -17,7 +17,16 @@ impl MigratorTrait for Migrator { } pub async fn migrate_database(db_url: &str) -> Result<(), DbErr> { - let db = Database::connect(db_url).await?; + let mut opt = ConnectOptions::new(db_url); + opt.max_connections(10) + .min_connections(0) + .connect_timeout(std::time::Duration::from_secs(8)) + .idle_timeout(std::time::Duration::from_secs(8)) + .test_before_acquire(true) + .sqlx_logging(true) + .sqlx_logging_level(log::LevelFilter::Debug); + let db = Database::connect(opt).await?; + Migrator::up(&db, None).await } -- 2.49.1 From e849b71a40bfcac800b10df58e74742b3ab3301a Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:42:44 +0800 Subject: [PATCH 03/11] Implement API setup with configuration management and startup tasks - Add `Cargo.toml` for API with dependencies. - Create `config.rs` for managing application settings. - Implement logging and server settings in `config.rs`. - Add `main.rs` to initialize the application and handle database connections. - Introduce `task` module with startup tasks, including database migrations. - Update `.gitignore` to exclude `config.yaml` and remove `.gitkeep`. --- Cargo.lock | 466 +++++++++++++++++++++++++++++++++- Cargo.toml | 1 + apps/api/.gitignore | 1 + apps/api/.gitkeep | 0 apps/api/Cargo.toml | 19 ++ apps/api/src/config.rs | 218 ++++++++++++++++ apps/api/src/main.rs | 113 +++++++++ apps/api/src/tasks.rs | 1 + apps/api/src/tasks/startup.rs | 25 ++ 9 files changed, 840 insertions(+), 4 deletions(-) create mode 100644 apps/api/.gitignore delete mode 100644 apps/api/.gitkeep create mode 100644 apps/api/Cargo.toml create mode 100644 apps/api/src/config.rs create mode 100644 apps/api/src/main.rs create mode 100644 apps/api/src/tasks.rs create mode 100644 apps/api/src/tasks/startup.rs diff --git a/Cargo.lock b/Cargo.lock index 9fec9ff..4acc510 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayvec" version = "0.7.6" @@ -153,6 +159,70 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "base64" version = "0.21.7" @@ -372,9 +442,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -382,9 +452,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -441,12 +511,52 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.15.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde-untagged", + "serde_core", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + [[package]] name = "container-simulate" version = "0.1.0" @@ -454,12 +564,22 @@ dependencies = [ "async-trait", "clap", "path-clean", + "serde_json", "shared", "testcontainers", "tokio", "url", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -525,6 +645,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -609,6 +735,7 @@ name = "database" version = "0.1.0" dependencies = [ "chrono", + "log", "migration", "sea-orm", "serde", @@ -682,6 +809,15 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "docker_credential" version = "1.3.2" @@ -714,12 +850,32 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -988,6 +1144,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -1400,6 +1562,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1479,6 +1652,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -1499,11 +1678,18 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" name = "migration" version = "0.1.0" dependencies = [ + "log", "sea-orm-cli", "sea-orm-migration", "tokio", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.1.0" @@ -1532,6 +1718,15 @@ dependencies = [ "tempfile", ] +[[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.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1659,6 +1854,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "ouroboros" version = "0.18.5" @@ -1743,6 +1948,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1758,6 +1969,49 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "pest_meta" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "pgvector" version = "0.4.1" @@ -2093,6 +2347,20 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ron" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +dependencies = [ + "bitflags 2.10.0", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + [[package]] name = "rsa" version = "0.9.9" @@ -2113,6 +2381,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rust_decimal" version = "1.39.0" @@ -2484,6 +2762,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -2517,6 +2807,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -2528,6 +2829,15 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2977,6 +3287,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "synstructure" version = "0.13.2" @@ -3096,6 +3412,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -3135,6 +3460,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -3198,6 +3524,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.7.3" @@ -3228,6 +3567,42 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "http", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -3264,6 +3639,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "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]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", ] [[package]] @@ -3272,13 +3669,21 @@ version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ + "chrono", "matchers", + "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", + "smallvec", "thread_local", + "time", "tracing", "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -3287,12 +3692,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -3320,6 +3737,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -3367,6 +3790,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -3834,12 +4263,41 @@ dependencies = [ "rustix", ] +[[package]] +name = "yaml-rust2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yet-another-nginx-proxy-manager" +version = "0.1.0" +dependencies = [ + "axum", + "chrono", + "config", + "database", + "migration", + "serde", + "serde_json", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index d44d56d..e969cee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "apps/api", "apps/container", "apps/cli", "public/shared", diff --git a/apps/api/.gitignore b/apps/api/.gitignore new file mode 100644 index 0000000..a539470 --- /dev/null +++ b/apps/api/.gitignore @@ -0,0 +1 @@ +config.yaml \ No newline at end of file diff --git a/apps/api/.gitkeep b/apps/api/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/api/Cargo.toml b/apps/api/Cargo.toml new file mode 100644 index 0000000..e57bfb0 --- /dev/null +++ b/apps/api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "yet-another-nginx-proxy-manager" +version = "0.1.0" +edition = "2024" + +[dependencies] +database = { path = "../../public/database" } +migration = { path = "../../public/migration" } + +axum = { version = "0.8.7", features = ["form", "http1", "json", "matched-path", "original-uri", "query", "tokio", "tower-log", "tracing", "macros"]} +chrono = { version = "0.4.42", features = ["clock", "std", "oldtime", "wasmbind", "serde"] } +config = { version = "0.15.19", features = ["toml", "json", "yaml", "ini", "ron", "json5", "convert-case", "async"] } +tokio = { version = "1", features = ["fs", "io-util", "io-std", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "sync", "time", "tracing"] } +tower = { version = "0.5.2", features = ["tokio", "tracing"] } +tower-http = { version = "0.6.6" } +tracing = { version = "0.1.41", features = ["std", "attributes"] } +tracing-subscriber = { version = "0.3.20", features = ["smallvec", "fmt", "ansi", "tracing-log", "std", "chrono", "json", "serde", "serde_json", "time", "tracing"] } +serde_json = { version = "1.0.145", features = ["std"] } +serde = { version = "1.0.228", features = ["std", "derive"] } diff --git a/apps/api/src/config.rs b/apps/api/src/config.rs new file mode 100644 index 0000000..745dca6 --- /dev/null +++ b/apps/api/src/config.rs @@ -0,0 +1,218 @@ +use std::net::IpAddr; + +use config::{Config, ConfigError}; +use tracing::{Level, debug, error, warn}; + +const LOGGING_LEVEL_KEY: &str = "LOGGING.LEVEL"; +const LOGGING_UTC_KEY: &str = "LOGGING.UTC"; +const SERVER_ADDRESS_KEY: &str = "SERVER.ADDRESS"; +const SERVER_PORT_KEY: &str = "SERVER.PORT"; +const DATABASE_URL_KEY: &str = "DATABASE.URL"; +const DATABASE_MAX_CONNECTIONS_KEY: &str = "DATABASE.MAX_CONNECTIONS"; +const DATABASE_MIGRATE_ON_STARTUP_KEY: &str = "DATABASE.MIGRATION.MIGRATE_ON_STARTUP"; + +trait FromConfig: Sized { + fn from_config(config: &Config) -> Result; + fn validate(&self) -> Result<(), String>; +} + +#[derive(Debug, Clone)] +pub struct ProgramSettings { + pub logging: LoggingSettings, + pub database: DatabaseSettings, + pub server: ServerSettings, +} + +#[derive(Debug, Clone)] +pub struct LoggingSettings { + pub level: Level, + pub utc: bool, +} + +#[derive(Debug, Clone)] +pub struct DatabaseSettings { + pub url: String, + pub max_connections: u32, + pub migrate_on_startup: bool, +} + +#[derive(Debug, Clone)] +pub struct ServerSettings { + pub address: IpAddr, + pub port: u16, +} + +impl FromConfig for ProgramSettings { + fn from_config(_config: &Config) -> Result { + let config = ProgramSettings { + logging: LoggingSettings::from_config(_config)?, + database: DatabaseSettings::from_config(_config)?, + server: ServerSettings::from_config(_config)?, + }; + config.validate()?; + Ok(config) + } + + fn validate(&self) -> Result<(), String> { + self.logging.validate()?; + self.database.validate()?; + self.server.validate()?; + Ok(()) + } +} + +impl FromConfig for LoggingSettings { + fn from_config(_config: &Config) -> Result { + const DEFAULT_LOGGING_LEVEL: Level = Level::INFO; + Ok(LoggingSettings { + level: _config + .get_string(LOGGING_LEVEL_KEY) + .unwrap_or_else(|err| { + warn!( + "Failed to read {} from configuration, defaulting to {}. Error: {}", + LOGGING_LEVEL_KEY, DEFAULT_LOGGING_LEVEL, err + ); + DEFAULT_LOGGING_LEVEL.to_string() + }) + .parse() + .unwrap_or_else(|err| { + warn!( + "Invalid logging level in configuration, defaulting to {}. Error: {}", + DEFAULT_LOGGING_LEVEL, err + ); + DEFAULT_LOGGING_LEVEL + }), + utc: _config + .get_bool(LOGGING_UTC_KEY) + .unwrap_or_else(|err: ConfigError| { + const DEFAULT_UTC: bool = false; + warn!( + "Invalid UTC setting in configuration, defaulting to {}. Error: {}", + DEFAULT_UTC, err + ); + DEFAULT_UTC + }), + }) + } + + fn validate(&self) -> Result<(), String> { + Ok(()) + } +} + +impl FromConfig for DatabaseSettings { + fn from_config(_config: &Config) -> Result { + Ok(DatabaseSettings { + url: _config + .get_string(DATABASE_URL_KEY) + .map_err(|op| match op { + ConfigError::NotFound(_) => "Database URL not found in configuration".into(), + err => { + format!("Failed to read Database URL from configuration {err}") + } + })?, + max_connections: _config + .get_int(DATABASE_MAX_CONNECTIONS_KEY) + .unwrap_or_else(|err| { + const DEFAULT_MAX_CONNECTIONS: i64 = 10; + warn!( + "{} not set or invalid in configuration, defaulting to {}. Error: {}", + DATABASE_MAX_CONNECTIONS_KEY, DEFAULT_MAX_CONNECTIONS, err + ); + DEFAULT_MAX_CONNECTIONS + }) as u32, + migrate_on_startup: _config + .get_bool(DATABASE_MIGRATE_ON_STARTUP_KEY) + .unwrap_or_else(|err| { + const DEFAULT_MIGRATE_ON_STARTUP: bool = true; + warn!( + "{} not set or invalid in configuration, defaulting to {}. Error: {}", + DATABASE_MIGRATE_ON_STARTUP_KEY, DEFAULT_MIGRATE_ON_STARTUP, err + ); + DEFAULT_MIGRATE_ON_STARTUP + }), + }) + } + + fn validate(&self) -> Result<(), String> { + Ok(()) + } +} + +impl FromConfig for ServerSettings { + fn from_config(_config: &Config) -> Result { + Ok(ServerSettings { + address: _config + .get_string(SERVER_ADDRESS_KEY) + .unwrap_or_else(|err| { + const DEFAULT_ADDRESS: &str = "0.0.0.0"; + match err { + ConfigError::NotFound(_) => {} + _ => { + warn!( + "Failed to read {} from configuration, defaulting to {}. Error: {}", + SERVER_ADDRESS_KEY, DEFAULT_ADDRESS, err + ); + } + }; + DEFAULT_ADDRESS.to_string() + }) + .parse() + .map_err(|e| format!("Invalid {} in configuration: {}", SERVER_ADDRESS_KEY, e))?, + + port: _config.get_int(SERVER_PORT_KEY).unwrap_or_else(|err| { + const DEFAULT_PORT: i64 = 8080; + warn!( + "{} not set or invalid in configuration, defaulting to {}. Error: {}", + SERVER_PORT_KEY, DEFAULT_PORT, err + ); + DEFAULT_PORT + }) as u16, + }) + } + + fn validate(&self) -> Result<(), String> { + #[allow(clippy::absurd_extreme_comparisons, unused_comparisons)] + if self.port == 0 || self.port > 65535 { + return Err("Server port must be between 1 and 65535".into()); + } + Ok(()) + } +} + +pub fn get_program_settings() -> ProgramSettings { + debug!("Loading program settings from configuration sources"); + let settings = Config::builder() + // dev / generated config has the highest priority (Overwrite by user config files) + .add_source(config::File::with_name("generated-config.yaml").required(false)) + // user config files + .add_source( + config::File::with_name("/etc/yet-another-nginx-proxy-manager/config").required(false), + ) + .add_source( + config::File::with_name("$HOME/.config/yet-another-nginx-proxy-manager/config") + .required(false), + ) + .add_source(config::File::with_name("config.yaml").required(false)) + // environment variables have the highest priority (Overwrite all config files) + .add_source( + config::Environment::with_prefix("YANPM") + .separator("__") + .prefix_separator("_"), + ) + .build() + .expect("Failed to build configuration"); + + debug!("Configuration sources loaded successfully"); + debug!("Parsing program settings from configuration"); + + ProgramSettings::from_config(&settings) + .inspect_err(|err| { + error!("Configuration error: {}", err); + debug!("Current configurations: {:#?}", settings); + }) + .inspect(|_| { + debug!("Program settings parsed successfully"); + }) + .expect("Failed to load program settings from configuration") +} diff --git a/apps/api/src/main.rs b/apps/api/src/main.rs new file mode 100644 index 0000000..b563267 --- /dev/null +++ b/apps/api/src/main.rs @@ -0,0 +1,113 @@ +mod config; +mod tasks; + +use axum::Router; +use database::{ConnectOptions, get_connection}; +use tracing::{debug, info}; +use tracing_subscriber::fmt::format::{DefaultFields, Format}; + +use crate::config::{LoggingSettings, ProgramSettings, get_program_settings}; + +#[tokio::main] +async fn main() { + // Temporary subscriber for initial logging during configuration reading + let make_temporary_subscriber = || { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .with_target(false) + .with_level(true) + .finish() + }; + + let settings = + tracing::subscriber::with_default(make_temporary_subscriber(), || -> ProgramSettings { + debug!("Temporary subscriber installed."); + info!("Reading configuration..."); + let settings = get_program_settings(); + info!("Configuration read successfully."); + debug!("Resetting global subscriber..."); + + let subscriber = get_global_tracing_subscriber_builder(&settings.logging).finish(); + tracing::subscriber::set_global_default(subscriber) + .expect("Failed to set global default subscriber"); + + debug!( + "Global subscriber set with logging level: {:?}", + settings.logging.level + ); + + settings + }); + + tasks::startup::run_startup_tasks(&settings) + .await + .expect("Failed to run startup tasks"); + + // setup database connection pool + info!("Establishing database connection..."); + debug!("Database URL: {}", settings.database.url); + + let db_options = |options: &mut ConnectOptions| { + options.max_connections(settings.database.max_connections); + }; + + let db_connection = get_connection(&settings.database.url, Some(db_options)) + .await + .expect("Failed to establish database connection"); + + info!("Database connection established."); + + // build the axum app and run the server... + info!("Starting application..."); + let app: Router = Router::new(); + let address = format!("{}:{}", settings.server.address, settings.server.port); + info!("Starting server at http://{}", address); + + let listener = tokio::net::TcpListener::bind(address) + .await + .expect("Failed to bind to address"); + + axum::serve(listener, app) + .await + .expect("Failed to run the server"); +} + +fn get_global_tracing_subscriber_builder( + settings: &LoggingSettings, +) -> tracing_subscriber::fmt::SubscriberBuilder< + DefaultFields, + Format, +> { + // After configuration is read, install the global subscriber + let builder = tracing_subscriber::fmt() + .with_max_level(settings.level) + .with_target(false) + .with_level(true); + + if settings.utc { + builder.with_timer(BoxedTimer(Box::new( + tracing_subscriber::fmt::time::UtcTime::rfc_3339(), + ))) + } else { + builder.with_timer(BoxedTimer(Box::new( + tracing_subscriber::fmt::time::ChronoLocal::rfc_3339(), + ))) + } +} + +// A small wrapper that holds a boxed `FormatTime` trait object and itself +// implements `FormatTime`, allowing us to use it as a concrete type with +// `builder.with_timer` while still picking the concrete timer implementation +// at runtime. +// wrapper type to hold boxed timers and implement the `FormatTime` trait for +// a concrete type so `with_timer` may be called once outside the conditional. +struct BoxedTimer(Box); + +impl tracing_subscriber::fmt::time::FormatTime for BoxedTimer { + fn format_time( + &self, + w: &mut tracing_subscriber::fmt::format::Writer<'_>, + ) -> std::result::Result<(), std::fmt::Error> { + self.0.format_time(w) + } +} diff --git a/apps/api/src/tasks.rs b/apps/api/src/tasks.rs new file mode 100644 index 0000000..564d8c8 --- /dev/null +++ b/apps/api/src/tasks.rs @@ -0,0 +1 @@ +pub mod startup; diff --git a/apps/api/src/tasks/startup.rs b/apps/api/src/tasks/startup.rs new file mode 100644 index 0000000..d4f1629 --- /dev/null +++ b/apps/api/src/tasks/startup.rs @@ -0,0 +1,25 @@ +use migration::migrate_database; +use tracing::{debug, info}; + +use crate::config::ProgramSettings; + +pub async fn run_startup_tasks(config: &ProgramSettings) -> Result<(), Box> { + // Here you can add any startup tasks you want to run when the application starts. + info!("Running startup tasks..."); + if config.database.migrate_on_startup { + run_database_migrations(&config.database.url).await?; + } else { + info!("Database migration on startup is disabled. Skipping migration."); + } + + Ok(()) +} + +async fn run_database_migrations(db_url: &str) -> Result<(), Box> { + // Logic to run database migrations + info!("Running database migrations..."); + debug!("Database URL: {}", db_url); + migrate_database(db_url).await.map_err(Box::new)?; + info!("Database migrations completed."); + Ok(()) +} -- 2.49.1 From bb622df89b7d662c9df3dc3805c4dfa53549732b Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:50:11 +0800 Subject: [PATCH 04/11] Basic route structure --- apps/api/src/main.rs | 7 ++++++- apps/api/src/routes.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/routes.rs diff --git a/apps/api/src/main.rs b/apps/api/src/main.rs index b563267..9529a15 100644 --- a/apps/api/src/main.rs +++ b/apps/api/src/main.rs @@ -1,4 +1,5 @@ mod config; +mod routes; mod tasks; use axum::Router; @@ -59,7 +60,11 @@ async fn main() { // build the axum app and run the server... info!("Starting application..."); - let app: Router = Router::new(); + let app: Router = routes::get_root_router(routes::AppState { + database_connection: db_connection, + service: std::sync::Arc::new(routes::AppService {}), + }); + let address = format!("{}:{}", settings.server.address, settings.server.port); info!("Starting server at http://{}", address); diff --git a/apps/api/src/routes.rs b/apps/api/src/routes.rs new file mode 100644 index 0000000..a3a3a32 --- /dev/null +++ b/apps/api/src/routes.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use axum::Router; +use migration::sea_orm::DatabaseConnection; + +pub struct AppState { + pub database_connection: DatabaseConnection, + pub service: Arc, +} + +pub struct AppService { + // +} + +pub fn get_root_router(state: impl Into>) -> Router { + let router = Router::new() + // TODO: Add routes + .with_state(state.into()); + + #[allow(clippy::let_and_return)] + router +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ensure_state_send_sync() { + fn assert_send_sync() {} + assert_send_sync::(); + } +} -- 2.49.1 From 537737b1cc53b5fa46d634e717aebd49fbb92f41 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:59:40 +0800 Subject: [PATCH 05/11] refactor configs into a crate --- apps/api/src/config.rs | 218 ------------------------------- apps/api/src/configs.rs | 76 +++++++++++ apps/api/src/configs/database.rs | 53 ++++++++ apps/api/src/configs/key.rs | 9 ++ apps/api/src/configs/logging.rs | 52 ++++++++ apps/api/src/configs/server.rs | 56 ++++++++ apps/api/src/main.rs | 4 +- apps/api/src/tasks/startup.rs | 2 +- 8 files changed, 249 insertions(+), 221 deletions(-) delete mode 100644 apps/api/src/config.rs create mode 100644 apps/api/src/configs.rs create mode 100644 apps/api/src/configs/database.rs create mode 100644 apps/api/src/configs/key.rs create mode 100644 apps/api/src/configs/logging.rs create mode 100644 apps/api/src/configs/server.rs diff --git a/apps/api/src/config.rs b/apps/api/src/config.rs deleted file mode 100644 index 745dca6..0000000 --- a/apps/api/src/config.rs +++ /dev/null @@ -1,218 +0,0 @@ -use std::net::IpAddr; - -use config::{Config, ConfigError}; -use tracing::{Level, debug, error, warn}; - -const LOGGING_LEVEL_KEY: &str = "LOGGING.LEVEL"; -const LOGGING_UTC_KEY: &str = "LOGGING.UTC"; -const SERVER_ADDRESS_KEY: &str = "SERVER.ADDRESS"; -const SERVER_PORT_KEY: &str = "SERVER.PORT"; -const DATABASE_URL_KEY: &str = "DATABASE.URL"; -const DATABASE_MAX_CONNECTIONS_KEY: &str = "DATABASE.MAX_CONNECTIONS"; -const DATABASE_MIGRATE_ON_STARTUP_KEY: &str = "DATABASE.MIGRATION.MIGRATE_ON_STARTUP"; - -trait FromConfig: Sized { - fn from_config(config: &Config) -> Result; - fn validate(&self) -> Result<(), String>; -} - -#[derive(Debug, Clone)] -pub struct ProgramSettings { - pub logging: LoggingSettings, - pub database: DatabaseSettings, - pub server: ServerSettings, -} - -#[derive(Debug, Clone)] -pub struct LoggingSettings { - pub level: Level, - pub utc: bool, -} - -#[derive(Debug, Clone)] -pub struct DatabaseSettings { - pub url: String, - pub max_connections: u32, - pub migrate_on_startup: bool, -} - -#[derive(Debug, Clone)] -pub struct ServerSettings { - pub address: IpAddr, - pub port: u16, -} - -impl FromConfig for ProgramSettings { - fn from_config(_config: &Config) -> Result { - let config = ProgramSettings { - logging: LoggingSettings::from_config(_config)?, - database: DatabaseSettings::from_config(_config)?, - server: ServerSettings::from_config(_config)?, - }; - config.validate()?; - Ok(config) - } - - fn validate(&self) -> Result<(), String> { - self.logging.validate()?; - self.database.validate()?; - self.server.validate()?; - Ok(()) - } -} - -impl FromConfig for LoggingSettings { - fn from_config(_config: &Config) -> Result { - const DEFAULT_LOGGING_LEVEL: Level = Level::INFO; - Ok(LoggingSettings { - level: _config - .get_string(LOGGING_LEVEL_KEY) - .unwrap_or_else(|err| { - warn!( - "Failed to read {} from configuration, defaulting to {}. Error: {}", - LOGGING_LEVEL_KEY, DEFAULT_LOGGING_LEVEL, err - ); - DEFAULT_LOGGING_LEVEL.to_string() - }) - .parse() - .unwrap_or_else(|err| { - warn!( - "Invalid logging level in configuration, defaulting to {}. Error: {}", - DEFAULT_LOGGING_LEVEL, err - ); - DEFAULT_LOGGING_LEVEL - }), - utc: _config - .get_bool(LOGGING_UTC_KEY) - .unwrap_or_else(|err: ConfigError| { - const DEFAULT_UTC: bool = false; - warn!( - "Invalid UTC setting in configuration, defaulting to {}. Error: {}", - DEFAULT_UTC, err - ); - DEFAULT_UTC - }), - }) - } - - fn validate(&self) -> Result<(), String> { - Ok(()) - } -} - -impl FromConfig for DatabaseSettings { - fn from_config(_config: &Config) -> Result { - Ok(DatabaseSettings { - url: _config - .get_string(DATABASE_URL_KEY) - .map_err(|op| match op { - ConfigError::NotFound(_) => "Database URL not found in configuration".into(), - err => { - format!("Failed to read Database URL from configuration {err}") - } - })?, - max_connections: _config - .get_int(DATABASE_MAX_CONNECTIONS_KEY) - .unwrap_or_else(|err| { - const DEFAULT_MAX_CONNECTIONS: i64 = 10; - warn!( - "{} not set or invalid in configuration, defaulting to {}. Error: {}", - DATABASE_MAX_CONNECTIONS_KEY, DEFAULT_MAX_CONNECTIONS, err - ); - DEFAULT_MAX_CONNECTIONS - }) as u32, - migrate_on_startup: _config - .get_bool(DATABASE_MIGRATE_ON_STARTUP_KEY) - .unwrap_or_else(|err| { - const DEFAULT_MIGRATE_ON_STARTUP: bool = true; - warn!( - "{} not set or invalid in configuration, defaulting to {}. Error: {}", - DATABASE_MIGRATE_ON_STARTUP_KEY, DEFAULT_MIGRATE_ON_STARTUP, err - ); - DEFAULT_MIGRATE_ON_STARTUP - }), - }) - } - - fn validate(&self) -> Result<(), String> { - Ok(()) - } -} - -impl FromConfig for ServerSettings { - fn from_config(_config: &Config) -> Result { - Ok(ServerSettings { - address: _config - .get_string(SERVER_ADDRESS_KEY) - .unwrap_or_else(|err| { - const DEFAULT_ADDRESS: &str = "0.0.0.0"; - match err { - ConfigError::NotFound(_) => {} - _ => { - warn!( - "Failed to read {} from configuration, defaulting to {}. Error: {}", - SERVER_ADDRESS_KEY, DEFAULT_ADDRESS, err - ); - } - }; - DEFAULT_ADDRESS.to_string() - }) - .parse() - .map_err(|e| format!("Invalid {} in configuration: {}", SERVER_ADDRESS_KEY, e))?, - - port: _config.get_int(SERVER_PORT_KEY).unwrap_or_else(|err| { - const DEFAULT_PORT: i64 = 8080; - warn!( - "{} not set or invalid in configuration, defaulting to {}. Error: {}", - SERVER_PORT_KEY, DEFAULT_PORT, err - ); - DEFAULT_PORT - }) as u16, - }) - } - - fn validate(&self) -> Result<(), String> { - #[allow(clippy::absurd_extreme_comparisons, unused_comparisons)] - if self.port == 0 || self.port > 65535 { - return Err("Server port must be between 1 and 65535".into()); - } - Ok(()) - } -} - -pub fn get_program_settings() -> ProgramSettings { - debug!("Loading program settings from configuration sources"); - let settings = Config::builder() - // dev / generated config has the highest priority (Overwrite by user config files) - .add_source(config::File::with_name("generated-config.yaml").required(false)) - // user config files - .add_source( - config::File::with_name("/etc/yet-another-nginx-proxy-manager/config").required(false), - ) - .add_source( - config::File::with_name("$HOME/.config/yet-another-nginx-proxy-manager/config") - .required(false), - ) - .add_source(config::File::with_name("config.yaml").required(false)) - // environment variables have the highest priority (Overwrite all config files) - .add_source( - config::Environment::with_prefix("YANPM") - .separator("__") - .prefix_separator("_"), - ) - .build() - .expect("Failed to build configuration"); - - debug!("Configuration sources loaded successfully"); - debug!("Parsing program settings from configuration"); - - ProgramSettings::from_config(&settings) - .inspect_err(|err| { - error!("Configuration error: {}", err); - debug!("Current configurations: {:#?}", settings); - }) - .inspect(|_| { - debug!("Program settings parsed successfully"); - }) - .expect("Failed to load program settings from configuration") -} diff --git a/apps/api/src/configs.rs b/apps/api/src/configs.rs new file mode 100644 index 0000000..cae85de --- /dev/null +++ b/apps/api/src/configs.rs @@ -0,0 +1,76 @@ +pub mod database; +pub mod logging; +pub mod server; + +mod key; + +use config::Config; +use tracing::{debug, error}; + +pub trait FromConfig: Sized { + fn from_config(config: &Config) -> Result; + fn validate(&self) -> Result<(), String>; +} + +#[derive(Debug, Clone)] +pub struct ProgramSettings { + pub logging: logging::LoggingSettings, + pub database: database::DatabaseSettings, + pub server: server::ServerSettings, +} + +impl FromConfig for ProgramSettings { + fn from_config(_config: &Config) -> Result { + let config = ProgramSettings { + logging: logging::LoggingSettings::from_config(_config)?, + database: database::DatabaseSettings::from_config(_config)?, + server: server::ServerSettings::from_config(_config)?, + }; + config.validate()?; + Ok(config) + } + + fn validate(&self) -> Result<(), String> { + self.logging.validate()?; + self.database.validate()?; + self.server.validate()?; + Ok(()) + } +} + +pub fn get_program_settings() -> ProgramSettings { + debug!("Loading program settings from configuration sources"); + let settings = Config::builder() + // dev / generated config has the highest priority (Overwrite by user config files) + .add_source(config::File::with_name("generated-config.yaml").required(false)) + // user config files + .add_source( + config::File::with_name("/etc/yet-another-nginx-proxy-manager/config").required(false), + ) + .add_source( + config::File::with_name("$HOME/.config/yet-another-nginx-proxy-manager/config") + .required(false), + ) + .add_source(config::File::with_name("config.yaml").required(false)) + // environment variables have the highest priority (Overwrite all config files) + .add_source( + config::Environment::with_prefix("YANPM") + .separator("__") + .prefix_separator("_"), + ) + .build() + .expect("Failed to build configuration"); + + debug!("Configuration sources loaded successfully"); + debug!("Parsing program settings from configuration"); + + ProgramSettings::from_config(&settings) + .inspect_err(|err| { + error!("Configuration error: {}", err); + debug!("Current configurations: {:#?}", settings); + }) + .inspect(|_| { + debug!("Program settings parsed successfully"); + }) + .expect("Failed to load program settings from configuration") +} diff --git a/apps/api/src/configs/database.rs b/apps/api/src/configs/database.rs new file mode 100644 index 0000000..25d9a78 --- /dev/null +++ b/apps/api/src/configs/database.rs @@ -0,0 +1,53 @@ +use config::{Config, ConfigError}; +use tracing::warn; + +use super::{ + FromConfig, + key::{DATABASE_MAX_CONNECTIONS_KEY, DATABASE_MIGRATE_ON_STARTUP_KEY}, +}; + +#[derive(Debug, Clone)] +pub struct DatabaseSettings { + pub url: String, + pub max_connections: u32, + pub migrate_on_startup: bool, +} + +impl FromConfig for DatabaseSettings { + fn from_config(_config: &Config) -> Result { + Ok(DatabaseSettings { + url: _config + .get_string(super::key::DATABASE_URL_KEY) + .map_err(|op| match op { + ConfigError::NotFound(_) => "Database URL not found in configuration".into(), + err => { + format!("Failed to read Database URL from configuration {err}") + } + })?, + max_connections: _config + .get_int(DATABASE_MAX_CONNECTIONS_KEY) + .unwrap_or_else(|err| { + const DEFAULT_MAX_CONNECTIONS: i64 = 10; + warn!( + "{} not set or invalid in configuration, defaulting to {}. Error: {}", + DATABASE_MAX_CONNECTIONS_KEY, DEFAULT_MAX_CONNECTIONS, err + ); + DEFAULT_MAX_CONNECTIONS + }) as u32, + migrate_on_startup: _config + .get_bool(DATABASE_MIGRATE_ON_STARTUP_KEY) + .unwrap_or_else(|err| { + const DEFAULT_MIGRATE_ON_STARTUP: bool = true; + warn!( + "{} not set or invalid in configuration, defaulting to {}. Error: {}", + DATABASE_MIGRATE_ON_STARTUP_KEY, DEFAULT_MIGRATE_ON_STARTUP, err + ); + DEFAULT_MIGRATE_ON_STARTUP + }), + }) + } + + fn validate(&self) -> Result<(), String> { + Ok(()) + } +} diff --git a/apps/api/src/configs/key.rs b/apps/api/src/configs/key.rs new file mode 100644 index 0000000..dd31902 --- /dev/null +++ b/apps/api/src/configs/key.rs @@ -0,0 +1,9 @@ +pub(crate) const LOGGING_LEVEL_KEY: &str = "LOGGING.LEVEL"; +pub(crate) const LOGGING_UTC_KEY: &str = "LOGGING.UTC"; +// +pub(crate) const SERVER_ADDRESS_KEY: &str = "SERVER.ADDRESS"; +pub(crate) const SERVER_PORT_KEY: &str = "SERVER.PORT"; +// +pub(crate) const DATABASE_URL_KEY: &str = "DATABASE.URL"; +pub(crate) const DATABASE_MAX_CONNECTIONS_KEY: &str = "DATABASE.MAX_CONNECTIONS"; +pub(crate) const DATABASE_MIGRATE_ON_STARTUP_KEY: &str = "DATABASE.MIGRATION.MIGRATE_ON_STARTUP"; diff --git a/apps/api/src/configs/logging.rs b/apps/api/src/configs/logging.rs new file mode 100644 index 0000000..1aa47b9 --- /dev/null +++ b/apps/api/src/configs/logging.rs @@ -0,0 +1,52 @@ +use config::{Config, ConfigError}; +use tracing::{Level, warn}; + +use super::{ + FromConfig, + key::{LOGGING_LEVEL_KEY, LOGGING_UTC_KEY}, +}; + +#[derive(Debug, Clone)] +pub struct LoggingSettings { + pub level: Level, + pub utc: bool, +} + +impl FromConfig for LoggingSettings { + fn from_config(_config: &Config) -> Result { + const DEFAULT_LOGGING_LEVEL: Level = Level::INFO; + Ok(LoggingSettings { + level: _config + .get_string(LOGGING_LEVEL_KEY) + .unwrap_or_else(|err| { + warn!( + "Failed to read {} from configuration, defaulting to {}. Error: {}", + LOGGING_LEVEL_KEY, DEFAULT_LOGGING_LEVEL, err + ); + DEFAULT_LOGGING_LEVEL.to_string() + }) + .parse() + .unwrap_or_else(|err| { + warn!( + "Invalid logging level in configuration, defaulting to {}. Error: {}", + DEFAULT_LOGGING_LEVEL, err + ); + DEFAULT_LOGGING_LEVEL + }), + utc: _config + .get_bool(LOGGING_UTC_KEY) + .unwrap_or_else(|err: ConfigError| { + const DEFAULT_UTC: bool = false; + warn!( + "Invalid UTC setting in configuration, defaulting to {}. Error: {}", + DEFAULT_UTC, err + ); + DEFAULT_UTC + }), + }) + } + + fn validate(&self) -> Result<(), String> { + Ok(()) + } +} diff --git a/apps/api/src/configs/server.rs b/apps/api/src/configs/server.rs new file mode 100644 index 0000000..16e6bee --- /dev/null +++ b/apps/api/src/configs/server.rs @@ -0,0 +1,56 @@ +use std::net::IpAddr; + +use config::{Config, ConfigError}; +use tracing::warn; + +use super::{ + FromConfig, + key::{SERVER_ADDRESS_KEY, SERVER_PORT_KEY}, +}; + +#[derive(Debug, Clone)] +pub struct ServerSettings { + pub address: IpAddr, + pub port: u16, +} + +impl FromConfig for ServerSettings { + fn from_config(_config: &Config) -> Result { + Ok(ServerSettings { + address: _config + .get_string(SERVER_ADDRESS_KEY) + .unwrap_or_else(|err| { + const DEFAULT_ADDRESS: &str = "0.0.0.0"; + match err { + ConfigError::NotFound(_) => {} + _ => { + warn!( + "Failed to read {} from configuration, defaulting to {}. Error: {}", + SERVER_ADDRESS_KEY, DEFAULT_ADDRESS, err + ); + } + }; + DEFAULT_ADDRESS.to_string() + }) + .parse() + .map_err(|e| format!("Invalid {} in configuration: {}", SERVER_ADDRESS_KEY, e))?, + + port: _config.get_int(SERVER_PORT_KEY).unwrap_or_else(|err| { + const DEFAULT_PORT: i64 = 8080; + warn!( + "{} not set or invalid in configuration, defaulting to {}. Error: {}", + SERVER_PORT_KEY, DEFAULT_PORT, err + ); + DEFAULT_PORT + }) as u16, + }) + } + + fn validate(&self) -> Result<(), String> { + #[allow(clippy::absurd_extreme_comparisons, unused_comparisons)] + if self.port == 0 || self.port > 65535 { + return Err("Server port must be between 1 and 65535".into()); + } + Ok(()) + } +} diff --git a/apps/api/src/main.rs b/apps/api/src/main.rs index 9529a15..6bcbb78 100644 --- a/apps/api/src/main.rs +++ b/apps/api/src/main.rs @@ -1,4 +1,4 @@ -mod config; +mod configs; mod routes; mod tasks; @@ -7,7 +7,7 @@ use database::{ConnectOptions, get_connection}; use tracing::{debug, info}; use tracing_subscriber::fmt::format::{DefaultFields, Format}; -use crate::config::{LoggingSettings, ProgramSettings, get_program_settings}; +use crate::configs::{ProgramSettings, get_program_settings, logging::LoggingSettings}; #[tokio::main] async fn main() { diff --git a/apps/api/src/tasks/startup.rs b/apps/api/src/tasks/startup.rs index d4f1629..17d150b 100644 --- a/apps/api/src/tasks/startup.rs +++ b/apps/api/src/tasks/startup.rs @@ -1,7 +1,7 @@ use migration::migrate_database; use tracing::{debug, info}; -use crate::config::ProgramSettings; +use crate::configs::ProgramSettings; pub async fn run_startup_tasks(config: &ProgramSettings) -> Result<(), Box> { // Here you can add any startup tasks you want to run when the application starts. -- 2.49.1 From 547d73fab7842e13b18b438d7c0c02974ff44540 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Thu, 27 Nov 2025 19:05:41 +0800 Subject: [PATCH 06/11] temp allow deadcode for AppState struct --- apps/api/src/routes.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/api/src/routes.rs b/apps/api/src/routes.rs index a3a3a32..d202ce2 100644 --- a/apps/api/src/routes.rs +++ b/apps/api/src/routes.rs @@ -4,7 +4,11 @@ use axum::Router; use migration::sea_orm::DatabaseConnection; pub struct AppState { + // TODO: remove dead_code allowances when fields are used + #[allow(dead_code)] pub database_connection: DatabaseConnection, + // TODO: remove dead_code allowances when fields are used + #[allow(dead_code)] pub service: Arc, } -- 2.49.1 From 6cd55d06a2ddaeb6e2bce28d781e911bb7650f9f Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:47:56 +0800 Subject: [PATCH 07/11] Implement basic middleware handling --- Cargo.lock | 15 --------------- apps/api/Cargo.toml | 3 +-- apps/api/src/main.rs | 1 + apps/api/src/middlewares.rs | 34 ++++++++++++++++++++++++++++++++++ apps/api/src/routes.rs | 14 +++++++++----- 5 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 apps/api/src/middlewares.rs diff --git a/Cargo.lock b/Cargo.lock index 4acc510..a0c4b44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3583,20 +3583,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.10.0", - "bytes", - "http", - "pin-project-lite", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-layer" version = "0.3.3" @@ -4293,7 +4279,6 @@ dependencies = [ "serde_json", "tokio", "tower", - "tower-http", "tracing", "tracing-subscriber", ] diff --git a/apps/api/Cargo.toml b/apps/api/Cargo.toml index e57bfb0..55b3f06 100644 --- a/apps/api/Cargo.toml +++ b/apps/api/Cargo.toml @@ -11,8 +11,7 @@ axum = { version = "0.8.7", features = ["form", "http1", "json", "matched-path", chrono = { version = "0.4.42", features = ["clock", "std", "oldtime", "wasmbind", "serde"] } config = { version = "0.15.19", features = ["toml", "json", "yaml", "ini", "ron", "json5", "convert-case", "async"] } tokio = { version = "1", features = ["fs", "io-util", "io-std", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "sync", "time", "tracing"] } -tower = { version = "0.5.2", features = ["tokio", "tracing"] } -tower-http = { version = "0.6.6" } +tower = { version = "0.5.2", features = ["tokio", "tracing", "timeout"] } tracing = { version = "0.1.41", features = ["std", "attributes"] } tracing-subscriber = { version = "0.3.20", features = ["smallvec", "fmt", "ansi", "tracing-log", "std", "chrono", "json", "serde", "serde_json", "time", "tracing"] } serde_json = { version = "1.0.145", features = ["std"] } diff --git a/apps/api/src/main.rs b/apps/api/src/main.rs index 6bcbb78..cb1256d 100644 --- a/apps/api/src/main.rs +++ b/apps/api/src/main.rs @@ -1,4 +1,5 @@ mod configs; +mod middlewares; mod routes; mod tasks; diff --git a/apps/api/src/middlewares.rs b/apps/api/src/middlewares.rs new file mode 100644 index 0000000..f47e3de --- /dev/null +++ b/apps/api/src/middlewares.rs @@ -0,0 +1,34 @@ +use axum::{ + BoxError, Router, + error_handling::HandleErrorLayer, + http::{Method, StatusCode, Uri}, +}; +use std::time::Duration; +use tower::{ServiceBuilder, timeout::TimeoutLayer}; + +use tracing::warn; + +pub const TIMEOUT_DURATION_SECS: u64 = 30; + +pub fn apply_root_middleware(router: Router) -> Router { + let timeout_layer = TimeoutLayer::new(Duration::from_secs(TIMEOUT_DURATION_SECS)); + + let service_builder = ServiceBuilder::new() + .layer(HandleErrorLayer::new(handle_timeout_error)) + .layer(timeout_layer); + + router.layer(service_builder) +} + +pub async fn handle_timeout_error( + method: Method, + uri: Uri, + // + err: BoxError, +) -> (StatusCode, String) { + warn!("`{method} {uri}` failed with {err}"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal server error".to_string(), + ) +} diff --git a/apps/api/src/routes.rs b/apps/api/src/routes.rs index d202ce2..89328a2 100644 --- a/apps/api/src/routes.rs +++ b/apps/api/src/routes.rs @@ -1,8 +1,11 @@ use std::sync::Arc; -use axum::Router; +use axum::{Extension, Router}; use migration::sea_orm::DatabaseConnection; +use crate::middlewares; + +#[derive(Clone)] pub struct AppState { // TODO: remove dead_code allowances when fields are used #[allow(dead_code)] @@ -17,11 +20,12 @@ pub struct AppService { } pub fn get_root_router(state: impl Into>) -> Router { - let router = Router::new() - // TODO: Add routes - .with_state(state.into()); + let mut router = Router::new(); + + router = middlewares::apply_root_middleware(router); + + router = router.layer(Extension(state.into())); - #[allow(clippy::let_and_return)] router } -- 2.49.1 From 8b98590a1e64936e256abd648dc7fd430c220be3 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:55:08 +0800 Subject: [PATCH 08/11] Add service_error module for error handling --- apps/api/src/errors.rs | 1 + apps/api/src/errors/service_error.rs | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 apps/api/src/errors.rs create mode 100644 apps/api/src/errors/service_error.rs diff --git a/apps/api/src/errors.rs b/apps/api/src/errors.rs new file mode 100644 index 0000000..ae13070 --- /dev/null +++ b/apps/api/src/errors.rs @@ -0,0 +1 @@ +pub mod service_error; diff --git a/apps/api/src/errors/service_error.rs b/apps/api/src/errors/service_error.rs new file mode 100644 index 0000000..ce8c3f8 --- /dev/null +++ b/apps/api/src/errors/service_error.rs @@ -0,0 +1,14 @@ +pub type ServiceError = Box; + +pub trait IntoServiceError { + fn into_service_error(self) -> ServiceError; +} + +impl IntoServiceError for T +where + T: std::error::Error + Send + Sync + 'static, +{ + fn into_service_error(self) -> ServiceError { + Box::new(self) + } +} -- 2.49.1 From fae951c90200fab8bde009594deb50adf91a7a03 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:55:39 +0800 Subject: [PATCH 09/11] Add async-trait and sea-orm dependencies; implement SettingsService for configuration management --- Cargo.lock | 2 + apps/api/Cargo.toml | 2 + apps/api/src/services.rs | 1 + apps/api/src/services/settings.rs | 89 +++++++++++++++++++++++++++++++ public/database/src/lib.rs | 2 +- 5 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/services.rs create mode 100644 apps/api/src/services/settings.rs diff --git a/Cargo.lock b/Cargo.lock index a0c4b44..f9f0ad6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4270,11 +4270,13 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" name = "yet-another-nginx-proxy-manager" version = "0.1.0" dependencies = [ + "async-trait", "axum", "chrono", "config", "database", "migration", + "sea-orm", "serde", "serde_json", "tokio", diff --git a/apps/api/Cargo.toml b/apps/api/Cargo.toml index 55b3f06..87132f3 100644 --- a/apps/api/Cargo.toml +++ b/apps/api/Cargo.toml @@ -8,6 +8,7 @@ database = { path = "../../public/database" } migration = { path = "../../public/migration" } axum = { version = "0.8.7", features = ["form", "http1", "json", "matched-path", "original-uri", "query", "tokio", "tower-log", "tracing", "macros"]} +async-trait = { version = "0.1.89" } chrono = { version = "0.4.42", features = ["clock", "std", "oldtime", "wasmbind", "serde"] } config = { version = "0.15.19", features = ["toml", "json", "yaml", "ini", "ron", "json5", "convert-case", "async"] } tokio = { version = "1", features = ["fs", "io-util", "io-std", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "sync", "time", "tracing"] } @@ -16,3 +17,4 @@ tracing = { version = "0.1.41", features = ["std", "attributes"] } tracing-subscriber = { version = "0.3.20", features = ["smallvec", "fmt", "ansi", "tracing-log", "std", "chrono", "json", "serde", "serde_json", "time", "tracing"] } serde_json = { version = "1.0.145", features = ["std"] } serde = { version = "1.0.228", features = ["std", "derive"] } +sea-orm = { version = "2.0.0-rc" } diff --git a/apps/api/src/services.rs b/apps/api/src/services.rs new file mode 100644 index 0000000..6e98cef --- /dev/null +++ b/apps/api/src/services.rs @@ -0,0 +1 @@ +pub mod settings; diff --git a/apps/api/src/services/settings.rs b/apps/api/src/services/settings.rs new file mode 100644 index 0000000..c778427 --- /dev/null +++ b/apps/api/src/services/settings.rs @@ -0,0 +1,89 @@ +use std::sync::Arc; + +use database::generated::entities::config::{self, ActiveModel as ConfigActiveModel}; + +use sea_orm::{ + ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, DbErr, EntityTrait, + IntoActiveModel, QueryFilter, +}; + +use crate::errors::service_error::{IntoServiceError, ServiceError}; + +#[async_trait::async_trait] +pub trait SettingsStore: Send + Sync { + async fn get_setting(&self, key: &str) -> Result; + async fn set_setting(&self, key: &str, value: String) -> Result<(), ServiceError>; +} + +pub struct SettingsService { + connection: Arc, +} + +impl SettingsService { + pub fn new(connection: Arc) -> Self { + Self { connection } + } +} + +#[async_trait::async_trait] +impl SettingsStore for SettingsService { + async fn get_setting(&self, key: &str) -> Result { + let setting = config::Entity::find() + .filter(config::Column::Key.eq(key)) + .one(&*self.connection) + .await; + + match setting { + Err(err) => Err(err.into_service_error()), + Ok(None) => Err( + DbErr::RecordNotFound(format!("Setting with key '{}' not found", key)) + .into_service_error(), + ), + Ok(Some(record)) => Ok(record.value), + } + } + + async fn set_setting(&self, key: &str, value: String) -> Result<(), ServiceError> { + let existing = config::Entity::find() + .filter(config::Column::Key.eq(key)) + .one(&*self.connection) + .await; + + let handle_not_found = async |key: String, value: String| { + let new_record = ConfigActiveModel { + key: ActiveValue::Set(key), + value: ActiveValue::Set(value), + created_at: ActiveValue::Set(chrono::Utc::now()), + updated_at: ActiveValue::Set(chrono::Utc::now()), + }; + new_record + .insert(&*self.connection) + .await + .map_err(|err| err.into_service_error()) + }; + + match existing { + Err(err) => match err { + DbErr::RecordNotFound(_) => { + handle_not_found(key.to_string(), value).await?; + } + _ => { + return Err(Box::new(err)); + } + }, + Ok(None) => { + handle_not_found(key.to_string(), value).await?; + } + Ok(Some(mut record)) => { + record.value = value; + record + .into_active_model() + .update(&*self.connection) + .await + .map_err(|err| err.into_service_error())?; + } + } + + Ok(()) + } +} diff --git a/public/database/src/lib.rs b/public/database/src/lib.rs index 7985e66..fb047b5 100644 --- a/public/database/src/lib.rs +++ b/public/database/src/lib.rs @@ -1,5 +1,5 @@ use log::LevelFilter; -pub use sea_orm::ConnectOptions; +use sea_orm::ConnectOptions; pub mod generated; pub async fn get_connection( -- 2.49.1 From f71cf370cd85f16ee1e6115412144ac2b22603cd Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:56:01 +0800 Subject: [PATCH 10/11] Refactor AppState and update database connection handling; integrate SettingsService --- apps/api/src/main.rs | 35 ++++++++++++++++++++++++++--------- apps/api/src/routes.rs | 8 +++++--- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/apps/api/src/main.rs b/apps/api/src/main.rs index cb1256d..dd1af30 100644 --- a/apps/api/src/main.rs +++ b/apps/api/src/main.rs @@ -1,14 +1,23 @@ mod configs; +mod errors; mod middlewares; mod routes; +mod services; mod tasks; +use std::sync::Arc; + use axum::Router; -use database::{ConnectOptions, get_connection}; +use database::get_connection; +use sea_orm::ConnectOptions; use tracing::{debug, info}; use tracing_subscriber::fmt::format::{DefaultFields, Format}; -use crate::configs::{ProgramSettings, get_program_settings, logging::LoggingSettings}; +use crate::{ + configs::{ProgramSettings, get_program_settings, logging::LoggingSettings}, + routes::{AppService, AppState}, + services::settings::SettingsService, +}; #[tokio::main] async fn main() { @@ -53,18 +62,17 @@ async fn main() { options.max_connections(settings.database.max_connections); }; - let db_connection = get_connection(&settings.database.url, Some(db_options)) - .await - .expect("Failed to establish database connection"); + let db_connection = Arc::new( + get_connection(&settings.database.url, Some(db_options)) + .await + .expect("Failed to establish database connection"), + ); info!("Database connection established."); // build the axum app and run the server... info!("Starting application..."); - let app: Router = routes::get_root_router(routes::AppState { - database_connection: db_connection, - service: std::sync::Arc::new(routes::AppService {}), - }); + let app: Router = routes::get_root_router(Arc::new(get_app_state(&db_connection))); let address = format!("{}:{}", settings.server.address, settings.server.port); info!("Starting server at http://{}", address); @@ -101,6 +109,15 @@ fn get_global_tracing_subscriber_builder( } } +fn get_app_state(db_connection: &Arc) -> AppState { + AppState { + database_connection: db_connection.clone(), + service: Arc::new(AppService { + settings: Arc::new(SettingsService::new(db_connection.clone())), + }), + } +} + // A small wrapper that holds a boxed `FormatTime` trait object and itself // implements `FormatTime`, allowing us to use it as a concrete type with // `builder.with_timer` while still picking the concrete timer implementation diff --git a/apps/api/src/routes.rs b/apps/api/src/routes.rs index 89328a2..ddb1b51 100644 --- a/apps/api/src/routes.rs +++ b/apps/api/src/routes.rs @@ -3,20 +3,22 @@ use std::sync::Arc; use axum::{Extension, Router}; use migration::sea_orm::DatabaseConnection; -use crate::middlewares; +use crate::{middlewares, services::settings::SettingsStore}; #[derive(Clone)] pub struct AppState { // TODO: remove dead_code allowances when fields are used #[allow(dead_code)] - pub database_connection: DatabaseConnection, + pub database_connection: Arc, // TODO: remove dead_code allowances when fields are used #[allow(dead_code)] pub service: Arc, } +pub type ServiceState = Arc; + pub struct AppService { - // + pub settings: ServiceState, } pub fn get_root_router(state: impl Into>) -> Router { -- 2.49.1 From f4e6eb56c8c3c23607aa2e22326b471fb1beec4f Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:12:24 +0800 Subject: [PATCH 11/11] Update sea-orm dependencies to use workspace configuration; add dead code annotations in service error handling and settings service --- Cargo.toml | 5 +++++ apps/api/Cargo.toml | 2 +- apps/api/src/errors/service_error.rs | 1 + apps/api/src/routes.rs | 3 ++- apps/api/src/services/settings.rs | 3 +++ public/database/Cargo.toml | 2 +- public/migration/Cargo.toml | 4 ++-- 7 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e969cee..9c3e719 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,8 @@ resolver = "3" [workspace.lints.clippy] module_inception = "allow" + +[workspace.dependencies] +sea-orm = "2.0.0-rc" +sea-orm-cli = "2.0.0-rc" +sea-orm-migration = "2.0.0-rc" diff --git a/apps/api/Cargo.toml b/apps/api/Cargo.toml index 87132f3..d5c120c 100644 --- a/apps/api/Cargo.toml +++ b/apps/api/Cargo.toml @@ -17,4 +17,4 @@ tracing = { version = "0.1.41", features = ["std", "attributes"] } tracing-subscriber = { version = "0.3.20", features = ["smallvec", "fmt", "ansi", "tracing-log", "std", "chrono", "json", "serde", "serde_json", "time", "tracing"] } serde_json = { version = "1.0.145", features = ["std"] } serde = { version = "1.0.228", features = ["std", "derive"] } -sea-orm = { version = "2.0.0-rc" } +sea-orm = { workspace = true } diff --git a/apps/api/src/errors/service_error.rs b/apps/api/src/errors/service_error.rs index ce8c3f8..20a5835 100644 --- a/apps/api/src/errors/service_error.rs +++ b/apps/api/src/errors/service_error.rs @@ -1,5 +1,6 @@ pub type ServiceError = Box; +#[allow(dead_code)] // TODO: remove when used pub trait IntoServiceError { fn into_service_error(self) -> ServiceError; } diff --git a/apps/api/src/routes.rs b/apps/api/src/routes.rs index ddb1b51..c0d22db 100644 --- a/apps/api/src/routes.rs +++ b/apps/api/src/routes.rs @@ -15,9 +15,10 @@ pub struct AppState { pub service: Arc, } -pub type ServiceState = Arc; +pub type ServiceState = Arc; pub struct AppService { + #[allow(dead_code)] // TODO: remove when used pub settings: ServiceState, } diff --git a/apps/api/src/services/settings.rs b/apps/api/src/services/settings.rs index c778427..847db61 100644 --- a/apps/api/src/services/settings.rs +++ b/apps/api/src/services/settings.rs @@ -11,11 +11,14 @@ use crate::errors::service_error::{IntoServiceError, ServiceError}; #[async_trait::async_trait] pub trait SettingsStore: Send + Sync { + #[allow(dead_code)] // TODO: remove when used async fn get_setting(&self, key: &str) -> Result; + #[allow(dead_code)] // TODO: remove when used async fn set_setting(&self, key: &str, value: String) -> Result<(), ServiceError>; } pub struct SettingsService { + #[allow(dead_code)] // TODO: remove when used connection: Arc, } diff --git a/public/database/Cargo.toml b/public/database/Cargo.toml index c180e41..76064c9 100644 --- a/public/database/Cargo.toml +++ b/public/database/Cargo.toml @@ -13,7 +13,7 @@ chrono = { version = "0.4", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.47.0", features = ["full"] } -sea-orm = { version = "2.0.0-rc", features = [ "sqlx-postgres", "sqlx-mysql", "sqlx-sqlite", "runtime-tokio-rustls", "macros", "mock", "with-chrono", "with-json", "with-uuid", "sqlite-use-returning-for-3_35", "mariadb-use-returning" ] } +sea-orm = { workspace = true, features = [ "sqlx-postgres", "sqlx-mysql", "sqlx-sqlite", "runtime-tokio-rustls", "macros", "mock", "with-chrono", "with-json", "with-uuid", "sqlite-use-returning-for-3_35", "mariadb-use-returning" ] } log = "0.4.28" [lints] diff --git a/public/migration/Cargo.toml b/public/migration/Cargo.toml index fa42db8..ed761e5 100644 --- a/public/migration/Cargo.toml +++ b/public/migration/Cargo.toml @@ -11,11 +11,11 @@ path = "src/lib.rs" [dependencies] tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } -sea-orm-cli = { version = "2.0.0-rc", features = ["sqlx-postgres", "sqlx-mysql", "sqlx-sqlite", "runtime-tokio"] } +sea-orm-cli = { workspace = true, features = ["sqlx-postgres", "sqlx-mysql", "sqlx-sqlite", "runtime-tokio"] } log = "0.4.28" [dependencies.sea-orm-migration] -version = "2.0.0-rc" +workspace = true features = [ "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature "sqlx-postgres", "sqlx-mysql", "sqlx-sqlite" # `DATABASE_DRIVER` features -- 2.49.1