Split up files and add templating

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-01-16 20:22:11 +01:00
parent 08a6e5b0fa
commit a1474dab43
8 changed files with 738 additions and 123 deletions

494
Cargo.lock generated
View file

@ -17,6 +17,15 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.21" version = "0.6.21"
@ -227,6 +236,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.10.0" version = "2.10.0"
@ -254,6 +269,16 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bstr"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.19.1" version = "3.19.1"
@ -294,6 +319,39 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"num-traits",
"windows-link",
]
[[package]]
name = "chrono-tz"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
dependencies = [
"chrono",
"chrono-tz-build",
"phf",
]
[[package]]
name = "chrono-tz-build"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
dependencies = [
"parse-zoneinfo",
"phf",
"phf_codegen",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.54" version = "4.5.54"
@ -366,6 +424,12 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@ -390,6 +454,25 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-queue" name = "crossbeam-queue"
version = "0.3.12" version = "0.3.12"
@ -448,6 +531,12 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "deunicode"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -541,6 +630,15 @@ dependencies = [
"regex-syntax", "regex-syntax",
] ]
[[package]]
name = "file-id"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9"
dependencies = [
"windows-sys 0.60.2",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.7" version = "0.1.7"
@ -579,6 +677,15 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.31" version = "0.3.31"
@ -702,6 +809,30 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "globset"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "globwalk"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
"bitflags 2.10.0",
"ignore",
"walkdir",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.5" version = "0.15.5"
@ -812,6 +943,15 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humansize"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
"libm",
]
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.8.1" version = "1.8.1"
@ -849,6 +989,30 @@ dependencies = [
"tower-service", "tower-service",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.1.1" version = "2.1.1"
@ -951,6 +1115,22 @@ dependencies = [
"icu_properties", "icu_properties",
] ]
[[package]]
name = "ignore"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.12.1" version = "2.12.1"
@ -961,6 +1141,26 @@ dependencies = [
"hashbrown 0.16.1", "hashbrown 0.16.1",
] ]
[[package]]
name = "inotify"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 2.10.0",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.2" version = "1.70.2"
@ -992,6 +1192,26 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "kqueue"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -1019,7 +1239,7 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.10.0",
"libc", "libc",
"redox_syscall 0.7.0", "redox_syscall 0.7.0",
] ]
@ -1113,6 +1333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [ dependencies = [
"libc", "libc",
"log",
"wasi", "wasi",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@ -1162,17 +1383,57 @@ dependencies = [
"axum", "axum",
"axum-login", "axum-login",
"displaydoc", "displaydoc",
"notify-debouncer-full",
"password-auth", "password-auth",
"serde", "serde",
"sqlx", "sqlx",
"tera",
"thiserror 2.0.17", "thiserror 2.0.17",
"tokio", "tokio",
"tower-livereload",
"tower-sessions", "tower-sessions",
"tower-sessions-sqlx-store", "tower-sessions-sqlx-store",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]]
name = "notify"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
"bitflags 2.10.0",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"notify-types",
"walkdir",
"windows-sys 0.60.2",
]
[[package]]
name = "notify-debouncer-full"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "375bd3a138be7bfeff3480e4a623df4cbfb55b79df617c055cd810ba466fa078"
dependencies = [
"file-id",
"log",
"notify",
"notify-types",
"walkdir",
]
[[package]]
name = "notify-types"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.50.3" version = "0.50.3"
@ -1275,6 +1536,15 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "parse-zoneinfo"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
dependencies = [
"regex",
]
[[package]] [[package]]
name = "password-auth" name = "password-auth"
version = "1.0.0" version = "1.0.0"
@ -1313,6 +1583,49 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
version = "2.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7"
dependencies = [
"memchr",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365"
dependencies = [
"pest",
"sha2",
]
[[package]] [[package]]
name = "petgraph" name = "petgraph"
version = "0.8.3" version = "0.8.3"
@ -1325,6 +1638,44 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@ -1442,7 +1793,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.10.0",
] ]
[[package]] [[package]]
@ -1451,7 +1802,19 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.10.0",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
] ]
[[package]] [[package]]
@ -1671,12 +2034,28 @@ dependencies = [
"rand_core", "rand_core",
] ]
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.11" version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "slug"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
dependencies = [
"deunicode",
"wasm-bindgen",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"
@ -1809,7 +2188,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64", "base64",
"bitflags", "bitflags 2.10.0",
"byteorder", "byteorder",
"bytes", "bytes",
"crc", "crc",
@ -1852,7 +2231,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64", "base64",
"bitflags", "bitflags 2.10.0",
"byteorder", "byteorder",
"crc", "crc",
"dotenvy", "dotenvy",
@ -1964,6 +2343,28 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tera"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722"
dependencies = [
"chrono",
"chrono-tz",
"globwalk",
"humansize",
"lazy_static",
"percent-encoding",
"pest",
"pest_derive",
"rand",
"regex",
"serde",
"serde_json",
"slug",
"unicode-segmentation",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@ -2071,9 +2472,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.48.0" version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
@ -2146,6 +2547,20 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-livereload"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62a893f6b4fbc35dc32eacd6b5cf61dd49abac153ac7b3892c5725b350912e25"
dependencies = [
"bytes",
"http",
"http-body",
"pin-project-lite",
"tokio",
"tower",
]
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.3" version = "0.3.3"
@ -2285,6 +2700,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.18" version = "0.3.18"
@ -2312,6 +2733,12 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.8" version = "2.5.8"
@ -2446,12 +2873,65 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View file

@ -20,3 +20,6 @@ displaydoc.workspace = true
password-auth = { workspace = true } password-auth = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing-subscriber = { workspace = true, features = ["env-filter"] }
tera = "1.20.1"
notify-debouncer-full = "0.6.0"
tower-livereload = "0.10.2"

View file

@ -1,54 +1,39 @@
use axum::Form; use std::sync::LazyLock;
use std::time::Duration;
use axum::Router; use axum::Router;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::Html; use axum::response::Html;
use axum::response::IntoResponse;
use axum::response::Redirect;
use axum::routing::get; use axum::routing::get;
use axum::routing::post;
use axum_login::AuthManagerLayerBuilder; use axum_login::AuthManagerLayerBuilder;
use axum_login::AuthUser;
use axum_login::AuthnBackend; use axum_login::AuthnBackend;
use axum_login::login_required; use axum_login::login_required;
use displaydoc::Display; use displaydoc::Display;
use password_auth::generate_hash; use notify_debouncer_full::DebouncedEvent;
use notify_debouncer_full::notify::EventKind;
use password_auth::verify_password; use password_auth::verify_password;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use sqlx::prelude::FromRow; use tera::Context;
use tera::Tera;
use thiserror::Error; use thiserror::Error;
use tokio::sync::RwLock;
use tokio::task; use tokio::task;
use tokio::task::AbortHandle; use tokio::task::AbortHandle;
use tower_livereload::LiveReloadLayer;
use tower_sessions::ExpiredDeletion; use tower_sessions::ExpiredDeletion;
use tower_sessions::SessionManagerLayer; use tower_sessions::SessionManagerLayer;
use tower_sessions_sqlx_store::SqliteStore; use tower_sessions_sqlx_store::SqliteStore;
use tracing::error;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
pub mod users;
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
run().await run().await
} }
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] type AuthSession = axum_login::AuthSession<Backend>;
struct User {
id: i64,
username: String,
password: String,
}
impl AuthUser for User {
type Id = i64;
fn id(&self) -> Self::Id {
self.id
}
fn session_auth_hash(&self) -> &[u8] {
self.password.as_bytes()
}
}
#[derive(Clone)] #[derive(Clone)]
struct Backend { struct Backend {
@ -56,7 +41,7 @@ struct Backend {
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
struct Credentials { struct UserCredentials {
username: String, username: String,
password: String, password: String,
} }
@ -71,8 +56,8 @@ pub enum AuthBackendError {
} }
impl AuthnBackend for Backend { impl AuthnBackend for Backend {
type User = User; type User = users::User;
type Credentials = Credentials; type Credentials = UserCredentials;
type Error = AuthBackendError; type Error = AuthBackendError;
async fn authenticate( async fn authenticate(
@ -85,7 +70,7 @@ impl AuthnBackend for Backend {
.await?; .await?;
task::spawn_blocking(move || { task::spawn_blocking(move || {
Ok(user.filter(|user| verify_password(&creds.password, &user.password).is_ok())) Ok(user.filter(|user| verify_password(&creds.password, user.password()).is_ok()))
}) })
.await? .await?
} }
@ -104,10 +89,13 @@ impl AuthnBackend for Backend {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct AppState { pub struct AppState {
pub db: SqlitePool, pub db: SqlitePool,
} }
pub static TERA: LazyLock<RwLock<Tera>> =
LazyLock::new(|| Tera::new("templates/**.tera.html").unwrap().into());
async fn run() -> anyhow::Result<()> { async fn run() -> anyhow::Result<()> {
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env()) .with_env_filter(EnvFilter::from_default_env())
@ -131,21 +119,54 @@ async fn run() -> anyhow::Result<()> {
let backend = Backend { db: db.clone() }; let backend = Backend { db: db.clone() };
let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build(); let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
let livereload = LiveReloadLayer::new();
let reloader = livereload.reloader();
let app = Router::new() let app = Router::new()
.route("/protected", get(show_protected)) .route("/protected", get(show_protected))
.route_layer(login_required!(Backend, login_url = "/login")) .route_layer(login_required!(Backend, login_url = "/login"))
.route("/login", get(show_login)) .merge(users::routes())
.route("/login", post(do_login))
.route("/register", get(show_register))
.route("/register", post(do_register))
.layer(auth_layer) .layer(auth_layer)
.layer(livereload)
.with_state(AppState { db }); .with_state(AppState { db });
let mut debouncer = notify_debouncer_full::new_debouncer(
Duration::from_millis(50),
None,
move |event: Result<Vec<DebouncedEvent>, Vec<notify_debouncer_full::notify::Error>>| {
match event {
Ok(events) => {
if events
.iter()
.any(|ev| !matches!(ev.event.kind, EventKind::Access(_)))
{
match TERA.blocking_write().full_reload() {
Ok(()) => {
reloader.reload();
}
Err(error) => {
error!("Could not reload tera templates: {error}");
}
}
}
}
Err(_e) => {}
}
},
)?;
debouncer.watch(
"templates/",
notify_debouncer_full::notify::RecursiveMode::Recursive,
)?;
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app.into_make_service()) axum::serve(listener, app.into_make_service())
.with_graceful_shutdown(shutdown_signal(deletion_task.abort_handle())) .with_graceful_shutdown(shutdown_signal(deletion_task.abort_handle()))
.await?; .await?;
debouncer.stop();
Ok(()) Ok(())
} }
@ -171,85 +192,14 @@ async fn shutdown_signal(handle: AbortHandle) {
handle.abort(); handle.abort();
} }
pub async fn render_template(name: &str, context: &Context) -> tera::Result<String> {
TERA.read().await.render(name, context)
}
async fn show_protected() -> Html<String> { async fn show_protected() -> Html<String> {
format!( Html(
r##" render_template("protected.tera.html", &Context::default())
<!DOCTYPE html> .await
<html> .unwrap(),
<body>
Yay, you did it!
</body>
</html>
"##
) )
.into()
}
async fn show_login() -> Html<String> {
format!(
r##"
<!DOCTYPE html>
<html>
<body>
<form action="/login" method="POST">
<input type="text" name="username"/>
<input type="password" name="password"/>
<input type="submit" />
</form>
</body>
</html>
"##
)
.into()
}
async fn show_register() -> Html<String> {
format!(
r##"
<!DOCTYPE html>
<html>
<body>
<form action="/register" method="POST">
<input type="text" name="username"/>
<input type="password" name="password"/>
<input type="submit" />
</form>
</body>
</html>
"##
)
.into()
}
type AuthSession = axum_login::AuthSession<Backend>;
async fn do_register(
app_state: State<AppState>,
Form(creds): Form<Credentials>,
) -> Result<impl IntoResponse, Html<String>> {
sqlx::query("INSERT INTO users (username, password) VALUES (?, ?)")
.bind(creds.username)
.bind(generate_hash(creds.password))
.execute(&app_state.db)
.await
.map_err(|err| Html(err.to_string()))?;
Ok(Redirect::to("/login").into_response())
}
async fn do_login(
mut auth_session: AuthSession,
Form(creds): Form<Credentials>,
) -> impl IntoResponse {
let user = match auth_session.authenticate(creds.clone()).await {
Ok(Some(user)) => user,
Ok(None) => return StatusCode::UNAUTHORIZED.into_response(),
Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
};
if auth_session.login(&user).await.is_err() {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
Redirect::to("/protected").into_response()
} }

View file

@ -0,0 +1,43 @@
use axum::Form;
use axum::http::StatusCode;
use axum::response::Html;
use axum::response::IntoResponse;
use axum::response::Redirect;
use crate::AuthSession;
use crate::UserCredentials;
pub async fn show_login() -> Html<String> {
format!(
r##"
<!DOCTYPE html>
<html>
<body>
<form action="/login" method="POST">
<input type="text" name="username"/>
<input type="password" name="password"/>
<input type="submit" />
</form>
</body>
</html>
"##
)
.into()
}
pub async fn do_login(
mut auth_session: AuthSession,
Form(creds): Form<UserCredentials>,
) -> impl IntoResponse {
let user = match auth_session.authenticate(creds.clone()).await {
Ok(Some(user)) => user,
Ok(None) => return StatusCode::UNAUTHORIZED.into_response(),
Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
};
if auth_session.login(&user).await.is_err() {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
Redirect::to("/protected").into_response()
}

View file

@ -0,0 +1,53 @@
use axum::Router;
use axum::routing::get;
use axum::routing::post;
use axum_login::AuthUser;
use serde::Deserialize;
use serde::Serialize;
use sqlx::FromRow;
use crate::AppState;
mod login;
mod register;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct User {
id: i64,
username: String,
password: String,
}
impl User {
pub fn id(&self) -> i64 {
self.id
}
pub fn username(&self) -> &str {
&self.username
}
pub fn password(&self) -> &str {
&self.password
}
}
impl AuthUser for User {
type Id = i64;
fn id(&self) -> Self::Id {
self.id
}
fn session_auth_hash(&self) -> &[u8] {
self.password.as_bytes()
}
}
pub fn routes() -> Router<AppState> {
Router::new()
.route("/login", get(login::show_login))
.route("/login", post(login::do_login))
.route("/register", get(register::show_register))
.route("/register", post(register::do_register))
}

View file

@ -0,0 +1,41 @@
use axum::Form;
use axum::extract::State;
use axum::response::Html;
use axum::response::IntoResponse;
use axum::response::Redirect;
use password_auth::generate_hash;
use crate::AppState;
use crate::UserCredentials;
pub async fn show_register() -> Html<String> {
format!(
r##"
<!DOCTYPE html>
<html>
<body>
<form action="/register" method="POST">
<input type="text" name="username"/>
<input type="password" name="password"/>
<input type="submit" />
</form>
</body>
</html>
"##
)
.into()
}
pub async fn do_register(
app_state: State<AppState>,
Form(creds): Form<UserCredentials>,
) -> Result<impl IntoResponse, Html<String>> {
sqlx::query("INSERT INTO users (username, password) VALUES (?, ?)")
.bind(creds.username)
.bind(generate_hash(creds.password))
.execute(&app_state.db)
.await
.map_err(|err| Html(err.to_string()))?;
Ok(Redirect::to("/login").into_response())
}

View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
{% block head %}
<title>{% block title %}{% endblock title%} - Nixie CI</title>
{% endblock head%}
</head>
<body class="min-h-screen flex flex-col">
<nav class="bg-orange-300 px-2 py-2 inset-shadow-2xs">
<div class="mx-auto container flex">
<a href="/" class="bg-gray-700 rounded text-white p-2 select-none">
<span class="text-green-200">>_</span>
Nixie CI ❄️
</a>
</div>
</nav>
<div class="grow px-2">
<div class="mx-auto container">
{% block content %}{% endblock content %}
</div>
</div>
<footer class="bg-orange-400 px-2 min-h-10 py-4 inset-shadow-2xs">
<div class="mx-auto container font-bold">
{% block footer %}
&copy; Copyright 2026 by Hemera
{% endblock footer %}
</div>
</footer>
</body>
</html>

View file

@ -0,0 +1,9 @@
{% extends "base.tera.html" %}
{% block title %}
Protected Page
{% endblock title %}
{% block content %}
This is super secret content!
{% endblock content %}