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(()) +}