From a667bfaf19309f741844d8257cc83b0d15ade46a Mon Sep 17 00:00:00 2001 From: Wyatt Herkamp Date: Fri, 6 Dec 2024 10:20:45 -0500 Subject: [PATCH] Added environment variable support to configs. --- Cargo.lock | 345 +++++++++++++------ Cargo.toml | 2 + crates/core/Cargo.toml | 1 + crates/core/src/database/config.rs | 110 ++++++ crates/core/src/database/mod.rs | 14 + crates/core/src/testing/mod.rs | 1 + nitro_repo/Cargo.toml | 5 +- nitro_repo/src/app/authentication/session.rs | 3 +- nitro_repo/src/app/config.rs | 245 ++++++------- nitro_repo/src/app/config/max_upload.rs | 198 +++++++++++ nitro_repo/src/app/config/security.rs | 63 ++++ nitro_repo/src/app/mod.rs | 19 +- nitro_repo/src/app/open_api.rs | 10 +- nitro_repo/src/app/web.rs | 57 +-- nitro_repo/src/config_editor.rs | 88 +++++ nitro_repo/src/main.rs | 55 ++- 16 files changed, 938 insertions(+), 278 deletions(-) create mode 100644 crates/core/src/database/config.rs create mode 100644 nitro_repo/src/app/config/max_upload.rs create mode 100644 nitro_repo/src/app/config/security.rs create mode 100644 nitro_repo/src/config_editor.rs diff --git a/Cargo.lock b/Cargo.lock index 4bd42e52..9ce9030d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,9 +67,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -137,9 +137,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "arbitrary" @@ -435,7 +435,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -452,6 +452,12 @@ dependencies = [ "which", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -524,9 +530,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "bzip2" @@ -551,9 +557,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" dependencies = [ "jobserver", "libc", @@ -629,9 +635,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -639,9 +645,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -663,9 +669,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cmake" @@ -789,6 +795,31 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1026,13 +1057,13 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "edit-xml" version = "0.1.0" -source = "git+https://github.com/wyatt-herkamp/edit-xml.git#1b3dae619320834680a7453310fc2b03af4f7bc2" +source = "git+https://github.com/wyatt-herkamp/edit-xml.git#3f3431bbb0835f6b14913a4ac5b45a23ef622f9f" dependencies = [ "ahash", "encoding_rs", "memchr", "quick-xml", - "thiserror 2.0.3", + "thiserror 2.0.4", "tracing", ] @@ -1090,12 +1121,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1269,6 +1300,24 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1329,7 +1378,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.6.0", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -1436,12 +1485,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hex" version = "0.4.3" @@ -1477,9 +1520,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -1511,9 +1554,9 @@ dependencies = [ [[package]] name = "http-range-header" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" [[package]] name = "httparse" @@ -1778,9 +1821,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -1796,6 +1839,23 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.6.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "ipnet" version = "2.10.1" @@ -1853,10 +1913,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1886,9 +1947,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lettre" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0161e452348e399deb685ba05e55ee116cae9410f4f51fe42d597361444521d9" +checksum = "ab4c9a167ff73df98a5ecc07e8bf5ce90b583665da3d1762eb1f775ad4d0d6f5" dependencies = [ "async-trait", "base64 0.22.1", @@ -1916,15 +1977,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.166" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] name = "libloading" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.52.6", @@ -1942,7 +2003,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", ] @@ -2019,7 +2080,7 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "maven-rs" version = "0.1.0" -source = "git+https://github.com/wyatt-herkamp/maven-rs.git#c729d9e44d49a814268e8c4b5bf16486703cd301" +source = "git+https://github.com/wyatt-herkamp/maven-rs.git#9f002814ffcf4e3ab7757ee9ee73abc323facef0" dependencies = [ "ahash", "chrono", @@ -2029,7 +2090,7 @@ dependencies = [ "quick-xml", "serde", "strum", - "thiserror 2.0.3", + "thiserror 2.0.4", "tracing", "winnow", ] @@ -2083,11 +2144,22 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "wasi", "windows-sys 0.52.0", @@ -2119,6 +2191,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nitro_repo" version = "2.0.0-BETA" @@ -2146,6 +2227,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", + "inquire", "lettre", "maven-rs", "mime", @@ -2168,12 +2250,13 @@ dependencies = [ "schemars", "semver", "serde", + "serde-env", "serde_json", "sha2", "sqlx", "strum", "tempfile", - "thiserror 2.0.3", + "thiserror 2.0.4", "tokio", "tokio-rustls", "toml", @@ -2211,6 +2294,7 @@ dependencies = [ "badge-maker", "base64 0.22.1", "chrono", + "clap", "derive_builder", "derive_more", "digestible", @@ -2225,7 +2309,7 @@ dependencies = [ "sha2", "sqlx", "strum", - "thiserror 2.0.3", + "thiserror 2.0.4", "tokio", "tracing", "tracing-appender", @@ -2272,7 +2356,7 @@ dependencies = [ "sha2", "sha3", "strum", - "thiserror 2.0.3", + "thiserror 2.0.4", "tokio", "tokio-util", "tracing", @@ -2628,9 +2712,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "postcard" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63d01def49fc815900a83e7a4a5083d2abc81b7ddd569a3fa0477778ae9b3ec" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" dependencies = [ "cobs", "embedded-io 0.4.0", @@ -2735,10 +2819,10 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.0.0", + "rustc-hash 2.1.0", "rustls", "socket2", - "thiserror 2.0.3", + "thiserror 2.0.4", "tokio", "tracing", ] @@ -2753,11 +2837,11 @@ dependencies = [ "getrandom", "rand", "ring", - "rustc-hash 2.0.0", + "rustc-hash 2.1.0", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.3", + "thiserror 2.0.4", "tinyvec", "tracing", "web-time", @@ -2837,7 +2921,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -3045,9 +3129,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustc_version" @@ -3064,7 +3148,7 @@ version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -3191,6 +3275,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-env" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13536c0c431652192b75c7d5afa83dedae98f91d7e687ff30a009e9d15284fb" +dependencies = [ + "anyhow", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.215" @@ -3318,6 +3412,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -3363,9 +3478,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3435,7 +3550,7 @@ dependencies = [ "hashbrown 0.14.5", "hashlink", "hex", - "indexmap 2.6.0", + "indexmap 2.7.0", "log", "memchr", "once_cell", @@ -3504,7 +3619,7 @@ checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" dependencies = [ "atoi", "base64 0.22.1", - "bitflags", + "bitflags 2.6.0", "byteorder", "bytes", "chrono", @@ -3548,7 +3663,7 @@ checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" dependencies = [ "atoi", "base64 0.22.1", - "bitflags", + "bitflags 2.6.0", "byteorder", "chrono", "crc", @@ -3671,9 +3786,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.89" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -3730,11 +3845,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490" dependencies = [ - "thiserror-impl 2.0.3", + "thiserror-impl 2.0.4", ] [[package]] @@ -3750,9 +3865,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061" dependencies = [ "proc-macro2", "quote", @@ -3771,9 +3886,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -3792,9 +3907,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -3827,14 +3942,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.1" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.3", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -3856,20 +3971,19 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ "rustls", - "rustls-pki-types", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -3878,9 +3992,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -3916,7 +4030,7 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.0", "serde", "serde_spanned", "toml_datetime", @@ -3998,7 +4112,7 @@ checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ "async-compression", "base64 0.22.1", - "bitflags", + "bitflags 2.6.0", "bytes", "futures-core", "futures-util", @@ -4109,9 +4223,9 @@ dependencies = [ [[package]] name = "tracing-serde" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" dependencies = [ "serde", "tracing-core", @@ -4119,9 +4233,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -4202,6 +4316,18 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -4255,7 +4381,7 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514a48569e4e21c86d0b84b5612b5e73c0b2cf09db63260134ba426d4e8ea714" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.0", "serde", "serde_json", "utoipa-gen", @@ -4349,9 +4475,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" dependencies = [ "cfg-if", "once_cell", @@ -4360,9 +4486,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" dependencies = [ "bumpalo", "log", @@ -4375,21 +4501,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.45" +version = "0.4.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +checksum = "9dfaf8f50e5f293737ee323940c7d8b08a66a95a419223d9f41610ca08b0833d" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4397,9 +4524,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" dependencies = [ "proc-macro2", "quote", @@ -4410,9 +4537,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" [[package]] name = "wasm-streams" @@ -4429,9 +4556,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "a98bc3c33f0fe7e59ad7cd041b89034fa82a7c2d4365ca538dda6cdaf513863c" dependencies = [ "js-sys", "wasm-bindgen", @@ -4847,13 +4974,13 @@ dependencies = [ "displaydoc", "flate2", "hmac", - "indexmap 2.6.0", + "indexmap 2.7.0", "lzma-rs", "memchr", "pbkdf2", "rand", "sha1", - "thiserror 2.0.3", + "thiserror 2.0.4", "time", "zeroize", "zopfli", diff --git a/Cargo.toml b/Cargo.toml index 3843e84d..4765be2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,8 @@ md-5 = "0.10" sha1 = "0.10" sha2 = "0.10" sha3 = "0.10" +clap = { version = "4", features = ["derive"] } + [workspace.dependencies.sqlx] version = "0.8" default-features = false diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 5019b2cf..f86e6d2c 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -35,6 +35,7 @@ tracing-subscriber = { version = "0.3", features = [ "json", ], optional = true } tracing-appender = { version = "0.2", optional = true } +clap.workspace = true [features] default = ["migrations", "testing"] migrations = [] diff --git a/crates/core/src/database/config.rs b/crates/core/src/database/config.rs new file mode 100644 index 00000000..48e98a70 --- /dev/null +++ b/crates/core/src/database/config.rs @@ -0,0 +1,110 @@ +use serde::{Deserialize, Serialize}; +use sqlx::postgres::PgConnectOptions; + +use super::DBError; + +/// The configuration for the database. +/// +/// Currently only supports PostgreSQL. +#[derive(Debug, Clone, Deserialize, Serialize, clap::Args)] +#[serde(default)] +pub struct DatabaseConfig { + /// The username to connect to the database. + /// + /// Default is `postgres`. + /// Environment_variable: NITRO-REPO_DATABASE_USER + #[clap(long = "database-user", default_value = "postgres")] + pub user: String, + /// The password to connect to the database. + /// + /// Default is `password`. + /// Environment_variable: NITRO-REPO_DATABASE_PASSWORD + #[clap(long = "database-password", default_value = "password")] + pub password: String, + #[clap(long = "database-name", default_value = "nitro_repo")] + #[serde(alias = "name")] + pub database: String, + // The host can be in the format host:port or just host. + #[clap(long = "database-host", default_value = "localhost:5432")] + pub host: String, + // The port is optional. If not specified the default port is used. or will be extracted from the host. + #[clap(long = "database-port")] + pub port: Option, +} +impl DatabaseConfig { + /// Returns the host and port + /// + /// If it is not specified in the port field it will attempt to extract it from the host field. + pub fn host_name_port(&self) -> Result<(&str, u16), DBError> { + if let Some(port) = self.port { + Ok((self.host.as_str(), port)) + } else { + // The port can be specified in the host field. If it is, we need to extract it. + let host = self.host.split(':').collect::>(); + + match host.len() { + // The port is not specified. Use the default port. + 1 => Ok((host[0], 5432)), + // The port is specified within the host. The port option is ignored. + 2 => Ok((host[0], host[1].parse::().unwrap_or(5432))), + _ => { + // Not in the format host:port. Possibly IPv6 but we don't support that. + // If it is IPv6 please specify the port separately. + return Err(DBError::InvalidHost(self.host.clone())); + } + } + } + } +} + +impl Default for DatabaseConfig { + fn default() -> Self { + Self { + user: "postgres".to_string(), + password: "password".to_string(), + database: "nitro_repo".to_string(), + host: "localhost".to_string(), + port: Some(5432), + } + } +} +impl TryFrom for PgConnectOptions { + type Error = DBError; + fn try_from(settings: DatabaseConfig) -> Result { + let (host, port) = settings.host_name_port()?; + let options = PgConnectOptions::new() + .username(&settings.user) + .password(&settings.password) + .host(host) + .port(port) + .database(&settings.database); + + Ok(options) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_host_name_port() { + { + let config = DatabaseConfig::default(); + let (host, port) = config.host_name_port().unwrap(); + assert_eq!(host, "localhost"); + assert_eq!(port, 5432); + } + { + let config = DatabaseConfig { + host: "localhost:5433".to_string(), + port: None, + ..DatabaseConfig::default() + }; + let (host, port) = config.host_name_port().unwrap(); + assert_eq!(host, "localhost"); + assert_eq!(port, 5433); + } + } +} diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index fdcb3e6a..4b3f5e62 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -5,4 +5,18 @@ pub mod repository; pub mod storage; pub mod user; pub type DateTime = chrono::DateTime; +mod config; pub mod stages; +pub use config::*; + +#[derive(thiserror::Error, Debug)] +pub enum DBError { + #[error(transparent)] + Sqlx(#[from] sqlx::Error), + #[error(transparent)] + Migration(#[from] sqlx::migrate::MigrateError), + #[error("{0}")] + Other(&'static str), + #[error("Invalid host must be in the format host:port got `{0}`")] + InvalidHost(String), +} diff --git a/crates/core/src/testing/mod.rs b/crates/core/src/testing/mod.rs index b41a1d88..4a2490b1 100644 --- a/crates/core/src/testing/mod.rs +++ b/crates/core/src/testing/mod.rs @@ -119,6 +119,7 @@ impl TestInfoEntry { #[cfg(test)] mod tests { + #[ignore = "Requires a database"] #[tokio::test] pub async fn test_test_core() { let (core, entry) = super::TestCore::new(format!("{}::test_test_core", module_path!())) diff --git a/nitro_repo/Cargo.toml b/nitro_repo/Cargo.toml index c15287a4..163355e9 100644 --- a/nitro_repo/Cargo.toml +++ b/nitro_repo/Cargo.toml @@ -38,6 +38,8 @@ sqlx.workspace = true # Serde serde.workspace = true serde_json.workspace = true +serde-env = "0.2" + toml.workspace = true # utils futures.workspace = true @@ -65,7 +67,7 @@ derive_more.workspace = true # Badge Stuff badge-maker.workspace = true pin-project = "1" -clap = { version = "4", features = ["derive"] } +clap.workspace = true semver = { version = "1", features = ["std", "serde"] } # Staging @@ -101,6 +103,7 @@ lettre = { version = "0.11.9", features = [ "tokio1-rustls-tls", ], default-features = false } url = "2" +inquire = "0.7" [features] default = ["utoipa-scalar"] builtin_frontend = [] diff --git a/nitro_repo/src/app/authentication/session.rs b/nitro_repo/src/app/authentication/session.rs index fca58765..7b3836b6 100644 --- a/nitro_repo/src/app/authentication/session.rs +++ b/nitro_repo/src/app/authentication/session.rs @@ -155,8 +155,7 @@ impl Debug for SessionManager { } } impl SessionManager { - pub fn new(session_config: Option, mode: Mode) -> Result { - let session_config = session_config.unwrap_or_default(); + pub fn new(session_config: SessionManagerConfig, mode: Mode) -> Result { let sessions = if session_config.database_location.exists() { let database = Database::open(&session_config.database_location)?; if mode == Mode::Debug { diff --git a/nitro_repo/src/app/config.rs b/nitro_repo/src/app/config.rs index 6e91032c..3a7295d3 100644 --- a/nitro_repo/src/app/config.rs +++ b/nitro_repo/src/app/config.rs @@ -1,21 +1,36 @@ +use nr_core::database::DatabaseConfig; use serde::{Deserialize, Serialize}; -use sqlx::postgres::PgConnectOptions; use std::env; +use std::fs::read_to_string; use std::path::PathBuf; use strum::EnumIs; -use tuxs_config_types::size_config::{ConfigSize, Unit}; +use tuxs_config_types::size_config::InvalidSizeError; use utoipa::ToSchema; - -use crate::repository::StagingConfig; - +mod max_upload; +mod security; use super::authentication::session::SessionManagerConfig; use super::email::EmailSetting; use super::logging::LoggingConfig; +use crate::repository::StagingConfig; +pub use max_upload::*; +pub use security::*; +pub const CONFIG_PREFIX: &str = "NITRO-REPO"; +#[derive(Debug, thiserror::Error)] +pub enum ConfigError { + #[error("Invalid size: {0}")] + InvalidSize(#[from] InvalidSizeError), + #[error("Invalid max upload size. Expected a valid size or 'unlimited', error: {error}, got: {value}")] + InvalidMaxUpload { + error: InvalidSizeError, + value: String, + }, +} #[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, EnumIs, ToSchema)] pub enum Mode { Debug, Release, } + impl Default for Mode { fn default() -> Self { #[cfg(debug_assertions)] @@ -27,138 +42,54 @@ impl Default for Mode { pub fn get_current_directory() -> PathBuf { env::current_dir().unwrap_or_else(|_| PathBuf::new()) } -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct SecuritySettings { - pub allow_basic_without_tokens: bool, - pub password_rules: Option, -} -impl Default for SecuritySettings { - fn default() -> Self { - Self { - allow_basic_without_tokens: false, - password_rules: Some(PasswordRules::default()), - } - } -} -#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] -pub struct PasswordRules { - pub min_length: usize, - pub require_uppercase: bool, - pub require_lowercase: bool, - pub require_number: bool, - pub require_symbol: bool, -} -impl PasswordRules { - pub fn validate(&self, password: &str) -> bool { - if password.len() < self.min_length { - return false; - } - if self.require_uppercase && !password.chars().any(|c| c.is_uppercase()) { - return false; - } - if self.require_lowercase && !password.chars().any(|c| c.is_lowercase()) { - return false; - } - if self.require_number && !password.chars().any(|c| c.is_numeric()) { - return false; - } - if self.require_symbol && !password.chars().any(|c| c.is_ascii_punctuation()) { - return false; - } - true - } -} -impl Default for PasswordRules { - fn default() -> Self { - Self { - min_length: 8, - require_uppercase: true, - require_lowercase: true, - require_number: true, - require_symbol: true, - } - } -} #[derive(Debug, Deserialize, Serialize, Clone, Default)] #[serde(default)] pub struct NitroRepoConfig { - pub database: Option, - pub log: Option, - pub bind_address: Option, - pub max_upload: Option, - pub server_workers: Option, + pub mode: Mode, + pub web_server: WebServer, + pub database: DatabaseConfig, + pub log: LoggingConfig, + pub sessions: SessionManagerConfig, + pub site: SiteSetting, + pub security: SecuritySettings, + pub staging: StagingConfig, + pub email: Option, +} +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +#[serde(default)] +pub struct ReadConfigType { pub mode: Option, + pub web_server: Option, + pub database: Option, + pub log: Option, pub sessions: Option, - pub tls: Option, pub email: Option, pub site: Option, pub security: Option, pub staging: Option, } - -impl NitroRepoConfig { - pub fn load(config_file: PathBuf, update_config: bool) -> anyhow::Result { - let app = if config_file.exists() { - let config = std::fs::read_to_string(&config_file)?; - let app: NitroRepoConfig = toml::from_str(&config)?; - if update_config { - let toml = toml::to_string_pretty(&app)?; - std::fs::write(&config_file, &toml)?; - } - app - } else { - let default = NitroRepoConfig::default(); - let config = toml::to_string_pretty(&default)?; - std::fs::write(&config_file, &config)?; - default - }; - Ok(app) - } -} -pub fn default_bind_address() -> String { - "0.0.0.0:6742".to_string() -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct PostgresSettings { - pub user: String, - pub password: String, - pub host: String, - pub database: String, +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(default)] +pub struct WebServer { + pub bind_address: String, + /// Should OpenAPI routes be enabled. + pub open_api_routes: bool, + /// The maximum upload size for the web server. + pub max_upload: MaxUpload, + /// The TLS configuration for the web server. + pub tls: Option, } -impl Default for PostgresSettings { +impl Default for WebServer { fn default() -> Self { Self { - user: "postgres".to_string(), - password: "password".to_string(), - host: "localhost:5432".to_string(), - database: "nitro_repo".to_string(), + bind_address: "0.0.0.0:6742".to_owned(), + open_api_routes: true, + max_upload: Default::default(), + tls: None, } } } -impl From for PgConnectOptions { - fn from(settings: PostgresSettings) -> Self { - let host = settings.host.split(':').collect::>(); - let (host, port) = match host.len() { - 1 => (host[0], 5432), - 2 => (host[0], host[1].parse::().unwrap_or(5432)), - _ => ("localhost", 5432), - }; - PgConnectOptions::new() - .username(&settings.user) - .password(&settings.password) - .host(host) - .port(port) - .database(&settings.database) - } -} -#[derive(Debug, Deserialize, Serialize, Clone, Default)] -pub struct TlsConfig { - pub private_key: PathBuf, - pub certificate_chain: PathBuf, -} - #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(default)] pub struct SiteSetting { @@ -179,3 +110,77 @@ impl Default for SiteSetting { } } } + +macro_rules! env_or_file_or_default { + ( + $config:ident, + $env:ident, + $key:ident + ) => { + $config.$key.or($env.$key).unwrap_or_default() + }; + ( $config:ident, $env:ident, $($key:ident),* ) => { + ( + $( + env_or_file_or_default!($config, $env, $key), + )* + ) + } +} +macro_rules! env_or_file_or_none { + ( + $config:ident, + $env:ident, + $key:ident + ) => { + $config.$key.or($env.$key) + }; + ( $config:ident, $env:ident, $($key:ident),* ) => { + ( + $( + env_or_file_or_none!($config, $env, $key), + )* + ) + } +} +/// Load the configuration from the environment or a configuration file. +/// +/// path: may not exist if it doesn't it will use the environment variables. +/// +/// Config File gets precedence over environment variables. +pub fn load_config(path: Option) -> anyhow::Result { + let environment: ReadConfigType = serde_env::from_env_with_prefix(CONFIG_PREFIX)?; + let config_from_file = if let Some(path) = path.filter(|path| path.exists() && path.is_file()) { + let contents = read_to_string(path)?; + toml::from_str(&contents)? + } else { + ReadConfigType::default() + }; + // Merge the environment variables with the configuration file. If neither exists the default values are used. + // Environment variables take precedence. + let (mode, web_server, database, log, sessions, site, security, staging) = env_or_file_or_default!( + config_from_file, + environment, + mode, + web_server, + database, + log, + sessions, + site, + security, + staging + ); + let email = env_or_file_or_none!(config_from_file, environment, email); + + Ok(NitroRepoConfig { + mode, + web_server, + database, + log, + sessions, + site, + security, + staging, + email, + }) +} diff --git a/nitro_repo/src/app/config/max_upload.rs b/nitro_repo/src/app/config/max_upload.rs new file mode 100644 index 00000000..c676654d --- /dev/null +++ b/nitro_repo/src/app/config/max_upload.rs @@ -0,0 +1,198 @@ +use std::{fmt::Display, str::FromStr}; + +use serde::{de::Visitor, Deserialize, Serialize}; +use tuxs_config_types::size_config::{ConfigSize, Unit as SizeUnit}; + +use super::ConfigError; + +/// The maximum upload size for the web server. +/// +/// If a number is provided it is assumed to be in bytes. +/// +/// 'unlimited' will remove the limit. +/// +/// Default is 100 MiB. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MaxUpload { + Limit(ConfigSize), + Unlimited, +} +impl From for axum::extract::DefaultBodyLimit { + fn from(value: MaxUpload) -> Self { + match value { + MaxUpload::Limit(size) => axum::extract::DefaultBodyLimit::max(size.size), + MaxUpload::Unlimited => axum::extract::DefaultBodyLimit::disable(), + } + } +} +impl Default for MaxUpload { + fn default() -> Self { + MaxUpload::Limit(ConfigSize { + size: 100, + unit: SizeUnit::Mebibytes, + }) + } +} + +impl Serialize for MaxUpload { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + super::MaxUpload::Limit(size) => size.serialize(serializer), + super::MaxUpload::Unlimited => serializer.serialize_str("unlimited"), + } + } +} +macro_rules! visit_num { + ( + $fn_name:ident => $type:ty + ) => { + fn $fn_name(self, v: $type) -> Result + where + E: serde::de::Error, + { + Ok(MaxUpload::from(v as usize)) + } + }; + ($( $fn_name:ident => $type:ty ),*) => { + $( + visit_num!($fn_name => $type); + )* + } + } +struct MaxUploadVisitor; + +impl<'de> Visitor<'de> for MaxUploadVisitor { + type Value = MaxUpload; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid size or 'unlimited'") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + MaxUpload::from_str(value).map_err(serde::de::Error::custom) + } + + visit_num!(visit_i64 => i64, visit_i32 => i32, visit_i16 => i16, visit_i8 => i8, visit_u64 => u64, visit_u32 => u32, visit_u16 => u16, visit_u8 => u8); +} +impl<'de> Deserialize<'de> for MaxUpload { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(MaxUploadVisitor) + } +} + +impl Display for MaxUpload { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MaxUpload::Limit(size) => write!(f, "{}", size), + MaxUpload::Unlimited => write!(f, "Unlimited"), + } + } +} +impl FromStr for MaxUpload { + type Err = ConfigError; + + fn from_str(s: &str) -> Result { + if s.to_lowercase() == "unlimited" { + Ok(MaxUpload::Unlimited) + } else { + let limit_value = + ConfigSize::from_str(s).map_err(|error| ConfigError::InvalidMaxUpload { + error, + value: s.to_owned(), + })?; + Ok(MaxUpload::Limit(limit_value)) + } + } +} +impl TryFrom<&str> for MaxUpload { + type Error = ConfigError; + + fn try_from(value: &str) -> Result { + MaxUpload::from_str(value) + } +} +impl TryFrom for MaxUpload { + type Error = ConfigError; + + fn try_from(value: String) -> Result { + MaxUpload::from_str(value.as_str()) + } +} +impl From for MaxUpload { + fn from(value: usize) -> Self { + MaxUpload::Limit(ConfigSize { + size: value, + unit: Default::default(), + }) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_max_upload_from_str() { + { + let max_upload = MaxUpload::from_str("100").unwrap(); + assert_eq!( + max_upload, + MaxUpload::Limit(ConfigSize { + size: 100, + unit: SizeUnit::Bytes, + }) + ); + } + { + let max_upload = MaxUpload::from_str("100b").unwrap(); + assert_eq!( + max_upload, + MaxUpload::Limit(ConfigSize { + size: 100, + unit: SizeUnit::Bytes, + }) + ); + } + { + let max_upload = MaxUpload::from_str("100KiB").unwrap(); + assert_eq!( + max_upload, + MaxUpload::Limit(ConfigSize { + size: 100, + unit: SizeUnit::Kibibytes, + }) + ); + } + { + let max_upload = MaxUpload::from_str("100MiB").unwrap(); + assert_eq!( + max_upload, + MaxUpload::Limit(ConfigSize { + size: 100, + unit: SizeUnit::Mebibytes, + }) + ); + } + } + #[test] + fn test_unlimited() { + { + let max_upload = MaxUpload::from_str("unlimited").unwrap(); + assert_eq!(max_upload, MaxUpload::Unlimited); + } + { + let max_upload = MaxUpload::from_str("Unlimited").unwrap(); + assert_eq!(max_upload, MaxUpload::Unlimited); + } + } +} diff --git a/nitro_repo/src/app/config/security.rs b/nitro_repo/src/app/config/security.rs new file mode 100644 index 00000000..53e104f3 --- /dev/null +++ b/nitro_repo/src/app/config/security.rs @@ -0,0 +1,63 @@ +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct SecuritySettings { + pub allow_basic_without_tokens: bool, + pub password_rules: Option, +} +impl Default for SecuritySettings { + fn default() -> Self { + Self { + allow_basic_without_tokens: false, + password_rules: Some(PasswordRules::default()), + } + } +} +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +pub struct PasswordRules { + pub min_length: usize, + pub require_uppercase: bool, + pub require_lowercase: bool, + pub require_number: bool, + pub require_symbol: bool, +} +impl PasswordRules { + pub fn validate(&self, password: &str) -> bool { + if password.len() < self.min_length { + return false; + } + if self.require_uppercase && !password.chars().any(|c| c.is_uppercase()) { + return false; + } + if self.require_lowercase && !password.chars().any(|c| c.is_lowercase()) { + return false; + } + if self.require_number && !password.chars().any(|c| c.is_numeric()) { + return false; + } + if self.require_symbol && !password.chars().any(|c| c.is_ascii_punctuation()) { + return false; + } + true + } +} +impl Default for PasswordRules { + fn default() -> Self { + Self { + min_length: 8, + require_uppercase: true, + require_lowercase: true, + require_number: true, + require_symbol: true, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct TlsConfig { + pub private_key: PathBuf, + pub certificate_chain: PathBuf, +} diff --git a/nitro_repo/src/app/mod.rs b/nitro_repo/src/app/mod.rs index a16ac0d2..26dbbcb8 100644 --- a/nitro_repo/src/app/mod.rs +++ b/nitro_repo/src/app/mod.rs @@ -5,7 +5,7 @@ use anyhow::Context; use authentication::session::{SessionManager, SessionManagerConfig}; use axum::extract::State; -use config::{Mode, PasswordRules, PostgresSettings, SecuritySettings, SiteSetting}; +use config::{Mode, PasswordRules, SecuritySettings, SiteSetting}; use derive_more::{derive::Deref, AsRef, Into}; use email::EmailSetting; use email_service::{EmailAccess, EmailService}; @@ -15,6 +15,7 @@ use nr_core::{ repository::DBRepository, storage::{DBStorage, StorageDBType}, user::user_utils, + DatabaseConfig, }, repository::config::{ project::ProjectConfigType, repository_page::RepositoryPageType, RepositoryConfigType, @@ -120,8 +121,8 @@ pub struct NitroRepo { pub email_access: Arc, } impl NitroRepo { - async fn load_database(database: PostgresSettings) -> anyhow::Result { - let database = PgPool::connect_with(database.into()) + async fn load_database(database: DatabaseConfig) -> anyhow::Result { + let database = PgPool::connect_with(database.try_into()?) .await .context("Could not connec to database")?; nr_core::database::migration::run_migrations(&database).await?; @@ -130,16 +131,16 @@ impl NitroRepo { pub async fn new( mode: Mode, site: SiteSetting, - security: Option, - session_manager: Option, - staging_config: Option, + security: SecuritySettings, + session_manager: SessionManagerConfig, + staging_config: StagingConfig, email_settings: Option, - database: PostgresSettings, + database: DatabaseConfig, ) -> anyhow::Result { let database = Self::load_database(database).await?; let is_installed = user_utils::does_user_exist(&database).await?; - let security = security.unwrap_or_default(); - let staging_config = staging_config.unwrap_or_default(); + let security = security; + let staging_config = staging_config; let instance = Instance { mode, version: current_semver!(), diff --git a/nitro_repo/src/app/open_api.rs b/nitro_repo/src/app/open_api.rs index 15e9aeb1..5be6f351 100644 --- a/nitro_repo/src/app/open_api.rs +++ b/nitro_repo/src/app/open_api.rs @@ -72,7 +72,10 @@ impl Modify for SecurityAddon { } } #[cfg(feature = "utoipa-scalar")] -pub fn build_router() -> axum::Router { +pub fn build_router() -> axum::Router +where + S: Clone + Send + Sync + 'static, +{ use utoipa_scalar::{Scalar, Servable}; Router::new() @@ -80,7 +83,10 @@ pub fn build_router() -> axum::Router { .merge(Scalar::with_url("/scalar", ApiDoc::openapi())) } #[cfg(not(feature = "utoipa-scalar"))] -pub fn build_router() -> axum::Router { +pub fn build_router() -> axum::Router +where + S: Clone + Send + Sync + 'static, +{ Router::new().route("/open-api-doc-raw", get(api_docs)) } async fn api_docs() -> Response { diff --git a/nitro_repo/src/app/web.rs b/nitro_repo/src/app/web.rs index 4b4aa63f..bd25f11e 100644 --- a/nitro_repo/src/app/web.rs +++ b/nitro_repo/src/app/web.rs @@ -1,7 +1,8 @@ use super::authentication::api_middleware::AuthenticationLayer; +use super::config::{load_config, WebServer}; use super::logging::request_tracing::NitroRepoTracing; use super::{api, config::NitroRepoConfig}; -use super::{config, NitroRepo}; +use super::{open_api, NitroRepo}; use anyhow::Context; use axum::extract::DefaultBodyLimit; @@ -23,42 +24,33 @@ use tokio_rustls::TlsAcceptor; use tower_http::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer}; use tower_http::set_header::SetResponseHeaderLayer; use tower_service::Service; -use tracing::{error, info, warn}; +use tracing::{debug, error, info, warn}; const REQUEST_ID_HEADER: HeaderName = HeaderName::from_static("x-request-id"); const POWERED_BY_HEADER: HeaderName = HeaderName::from_static("x-powered-by"); const POWERED_BY_VALUE: HeaderValue = HeaderValue::from_static("Nitro Repo"); -pub(crate) async fn start(config_path: PathBuf, add_defaults: bool) -> anyhow::Result<()> { +pub(crate) async fn start(config_path: Option) -> anyhow::Result<()> { let NitroRepoConfig { + web_server, database, log, - bind_address, - max_upload, mode, sessions, - tls, staging: staging_config, site, security, email, - .. - } = NitroRepoConfig::load(config_path, add_defaults)?; - let bind_address = bind_address.unwrap_or_else(config::default_bind_address); - let max_upload = max_upload - .map(|size| size.get_as_bytes()) - .unwrap_or(100 * 1024 * 1024); - let Some(database) = database else { - return Err(anyhow::anyhow!("Database Settings are Required")); - }; - let mode = mode.unwrap_or_default(); - let site = site.unwrap_or_default(); - log.unwrap_or_default().init(mode)?; - let tls = tls - .map(|tls| { - rustls_server_config(tls.private_key, tls.certificate_chain) - .context("Failed to create TLS configuration") - }) - .transpose()?; + } = load_config(config_path)?; + let WebServer { + bind_address, + max_upload, + tls, + open_api_routes, + } = web_server; + + let mode = mode; + let site = site; + log.init(mode)?; let site = NitroRepo::new( mode, @@ -74,7 +66,7 @@ pub(crate) async fn start(config_path: PathBuf, add_defaults: bool) -> anyhow::R let cloned_site = site.clone(); let auth_layer = AuthenticationLayer::from(site.clone()); - let app = Router::new() + let mut app = Router::new() .route( "/repositories/:storage/:repository/*path", any(crate::repository::handle_repo_request), @@ -93,9 +85,13 @@ pub(crate) async fn start(config_path: PathBuf, add_defaults: bool) -> anyhow::R ) .nest("/api", api::api_routes()) .nest("/badge", super::badge::badge_routes()) - .merge(super::open_api::build_router()) .with_state(site); + if open_api_routes { + info!("OpenAPI routes enabled"); + app = app.merge(open_api::build_router()) + } + let body_limit: DefaultBodyLimit = max_upload.into(); let app = app .layer(SetResponseHeaderLayer::if_not_present( POWERED_BY_HEADER, @@ -103,15 +99,22 @@ pub(crate) async fn start(config_path: PathBuf, add_defaults: bool) -> anyhow::R )) .layer(NitroRepoTracing::new_trace_layer()) .layer(PropagateRequestIdLayer::new(REQUEST_ID_HEADER)) - .layer(DefaultBodyLimit::max(max_upload)) + .layer(body_limit) .layer(SetRequestIdLayer::new(REQUEST_ID_HEADER, MakeRequestUuid)) .layer(auth_layer); + if let Some(tls) = tls { + debug!("Starting TLS server"); + let tls = rustls_server_config(tls.private_key, tls.certificate_chain) + .context("Failed to create TLS configuration")?; start_app_with_tls(tls, app, bind_address).await?; } else { + debug!("Starting non-TLS server"); start_app(app, bind_address, cloned_site).await?; } + info!("Server shutdown... Goodbye!"); + Ok(()) } async fn start_app(app: Router, bind: String, site: NitroRepo) -> anyhow::Result<()> { diff --git a/nitro_repo/src/config_editor.rs b/nitro_repo/src/config_editor.rs new file mode 100644 index 00000000..cbd60f48 --- /dev/null +++ b/nitro_repo/src/config_editor.rs @@ -0,0 +1,88 @@ +use std::path::PathBuf; + +use inquire::{validator::Validation, Text}; +use nr_core::database::DatabaseConfig; +use sqlx::{postgres::PgConnectOptions, Connection, PgConnection}; + +use crate::app::config::ReadConfigType; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] +pub enum ConfigSection { + Database, +} + +pub async fn editor(section: ConfigSection, config_path: PathBuf) -> anyhow::Result<()> { + let mut config = if config_path.exists() { + let config = std::fs::read_to_string(&config_path)?; + toml::from_str(&config)? + } else { + ReadConfigType::default() + }; + + match section { + ConfigSection::Database => { + let new_database = edit_database(config.database.unwrap_or_default()).await?; + config.database = Some(new_database); + } + } + + let new_config = toml::to_string_pretty(&config)?; + + std::fs::write(&config_path, new_config)?; + Ok(()) +} + +async fn edit_database(database: DatabaseConfig) -> anyhow::Result { + let port_validator = |port: &str| { + if let Ok(number) = port.parse::() { + if number > 0 && number < 65535 { + Ok(Validation::Valid) + } else { + Ok(Validation::Invalid( + "Port must be between 1 and 65535".into(), + )) + } + } else { + Ok(Validation::Invalid("Port must be a number".into())) + } + }; + let old_database_port = database + .port + .map(|p| p.to_string()) + .unwrap_or_else(|| "5432".to_string()); + let host = Text::new("Database Host") + .with_default(&database.host) + .prompt()?; + let port = Text::new("Database Port") + .with_default(&old_database_port) + .with_validator(port_validator) + .prompt()?; + let port_as_number = port.parse::()?; + + let user = Text::new("Database User") + .with_default(&database.user) + .prompt()?; + + let password = Text::new("Database Password") + .with_default(&database.password) + .prompt()?; + + let database_name = Text::new("Database Name") + .with_default(&database.database) + .prompt()?; + + let database = DatabaseConfig { + host, + port: Some(port_as_number), + user, + password, + database: database_name, + }; + { + let options: PgConnectOptions = database.clone().try_into()?; + let mut conn = PgConnection::connect_with(&options).await?; + conn.ping().await?; + conn.close().await?; + } + Ok(database) +} diff --git a/nitro_repo/src/main.rs b/nitro_repo/src/main.rs index 3b10c221..d01cb1eb 100644 --- a/nitro_repo/src/main.rs +++ b/nitro_repo/src/main.rs @@ -3,8 +3,11 @@ use std::{ sync::atomic::{AtomicUsize, Ordering}, }; +use app::config::NitroRepoConfig; use clap::{Parser, Subcommand}; +use config_editor::ConfigSection; pub mod app; +mod config_editor; pub mod error; mod exporter; pub mod repository; @@ -23,12 +26,23 @@ struct Command { #[derive(Subcommand, Clone, Debug)] enum SubCommands { Start { - // Comments will be destroyed by TOML - #[clap(long, default_value = "false")] - add_defaults_to_config: bool, + /// The thd-helper config file + #[clap(short, long)] + config: Option, + }, + SaveConfig { /// The thd-helper config file #[clap(short, long, default_value = "nitro_repo.toml")] config: PathBuf, + /// If it should add defaults if the file already exists. + #[clap(short, long, default_value = "false")] + add_defaults: bool, + }, + Config { + /// The thd-helper config file + #[clap(short, long, default_value = "nitro_repo.toml")] + config: PathBuf, + section: ConfigSection, }, Export { export: ExportOptions, @@ -47,24 +61,49 @@ fn main() -> anyhow::Result<()> { let command = Command::parse(); match command.sub_command { - SubCommands::Start { - add_defaults_to_config, + SubCommands::Start { config } => web_start(config), + SubCommands::SaveConfig { config, - } => web_start(add_defaults_to_config, config), + add_defaults, + } => save_config(config, add_defaults), SubCommands::Export { export, location } => match export { ExportOptions::RepositoryConfigTypes => exporter::export_repository_configs(location), ExportOptions::RepositoryTypes => exporter::export_repository_types(location), ExportOptions::OpenAPI => exporter::export_openapi(location), }, + SubCommands::Config { config, section } => { + let tokio = tokio::runtime::Builder::new_current_thread() + .thread_name_fn(thread_name) + .enable_all() + .build()?; + tokio.block_on(config_editor::editor(section, config)) + } } } -fn web_start(add_defaults: bool, config_path: PathBuf) -> anyhow::Result<()> { +fn web_start(config_path: Option) -> anyhow::Result<()> { let tokio = tokio::runtime::Builder::new_current_thread() .thread_name_fn(thread_name) .enable_all() .build()?; - tokio.block_on(app::web::start(config_path, add_defaults)) + tokio.block_on(app::web::start(config_path)) +} +fn save_config(config_path: PathBuf, add_defaults: bool) -> anyhow::Result<()> { + if config_path.exists() && !add_defaults { + anyhow::bail!("Config file already exists. Please remove it first. or use the --add-defaults flag to overwrite it."); + } + if config_path.is_dir() { + anyhow::bail!("Config file is a directory. Please pass a file path."); + } + let config = if config_path.exists() { + let config = std::fs::read_to_string(&config_path)?; + toml::from_str(&config)? + } else { + NitroRepoConfig::default() + }; + let contents = toml::to_string_pretty(&config)?; + std::fs::write(config_path, contents)?; + Ok(()) } fn thread_name() -> String { static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);