From f859f8f34b3aa5548b5245557378a9a904db545d Mon Sep 17 00:00:00 2001 From: Matthew Pomes Date: Sat, 4 Oct 2025 16:04:19 -0500 Subject: [PATCH] Add initial setup and admin page --- Cargo.lock | 482 +++++++++++++++++++++++++++++- Cargo.toml | 2 + Dockerfile | 10 + Rocket.toml | 10 +- src/admin.rs | 173 +++++++++++ src/auth.rs | 113 +++++-- src/main.rs | 12 +- templates/401.html.tera | 7 + templates/_basic.html.tera | 17 ++ templates/_layout.html.tera | 48 +++ templates/account.html.tera | 15 + templates/admin.html.tera | 49 +++ templates/dash.html.tera | 2 + templates/initial_setup.html.tera | 15 + templates/login.html.tera | 15 + 15 files changed, 926 insertions(+), 44 deletions(-) create mode 100644 Dockerfile create mode 100644 src/admin.rs create mode 100644 templates/401.html.tera create mode 100644 templates/_basic.html.tera create mode 100644 templates/_layout.html.tera create mode 100644 templates/account.html.tera create mode 100644 templates/admin.html.tera create mode 100644 templates/dash.html.tera create mode 100644 templates/initial_setup.html.tera create mode 100644 templates/login.html.tera diff --git a/Cargo.lock b/Cargo.lock index 36ecde0..e6ba48e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,7 +220,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cexpr", "clang-sys", "itertools", @@ -237,6 +237,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.9.4" @@ -264,6 +270,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -327,7 +343,29 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.1.3", +] + +[[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]] @@ -422,6 +460,25 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "crossbeam-queue" version = "0.3.12" @@ -488,6 +545,12 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + [[package]] name = "devise" version = "0.4.2" @@ -514,7 +577,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" dependencies = [ - "bitflags", + "bitflags 2.9.4", "proc-macro2", "proc-macro2-diagnostics", "quote", @@ -632,6 +695,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "find-msvc-tools" version = "0.1.1" @@ -676,6 +751,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.31" @@ -854,6 +938,30 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +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.9.4", + "ignore", + "walkdir", +] + [[package]] name = "gpodder-rs" version = "0.1.0" @@ -861,10 +969,12 @@ dependencies = [ "argon2", "base64", "chrono", + "either", "lazy_static", "password-hash", "rocket", "rocket_db_pools", + "rocket_dyn_templates", "semver", "sqlx", ] @@ -1012,6 +1122,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "hyper" version = "1.7.0" @@ -1180,6 +1299,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "2.11.0" @@ -1197,6 +1332,26 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "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]] name = "inout" version = "0.1.4" @@ -1206,6 +1361,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "intrusive-collections" version = "0.9.7" @@ -1221,7 +1385,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -1272,6 +1436,26 @@ dependencies = [ "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]] name = "lazy_static" version = "1.5.0" @@ -1315,7 +1499,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags", + "bitflags 2.9.4", "libc", "redox_syscall", ] @@ -1442,6 +1626,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -1476,6 +1661,43 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "normpath" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b" +dependencies = [ + "windows-sys 0.61.1", +] + +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.9.4", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", +] + [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -1608,6 +1830,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -1657,6 +1888,87 @@ 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", +] + +[[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 = "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 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1885,7 +2197,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags", + "bitflags 2.9.4", ] [[package]] @@ -2040,6 +2352,17 @@ dependencies = [ "quote", ] +[[package]] +name = "rocket_dyn_templates" +version = "0.1.0" +dependencies = [ + "normpath", + "notify", + "rocket", + "tera", + "walkdir", +] + [[package]] name = "rocket_http" version = "0.6.0-dev" @@ -2098,7 +2421,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2111,7 +2434,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.9.4", @@ -2306,6 +2629,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2448,6 +2780,16 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "smallvec" version = "1.15.1" @@ -2581,7 +2923,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", - "bitflags", + "bitflags 2.9.4", "byteorder", "bytes", "crc", @@ -2623,7 +2965,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", - "bitflags", + "bitflags 2.9.4", "byteorder", "crc", "dotenvy", @@ -2752,6 +3094,28 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "tera" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3062,6 +3426,12 @@ dependencies = [ "serde", ] +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uncased" version = "0.9.10" @@ -3072,6 +3442,56 @@ dependencies = [ "version_check", ] +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -3163,6 +3583,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -3289,6 +3719,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.60.2", +] + [[package]] name = "windows" version = "0.48.0" @@ -3306,7 +3745,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -3339,13 +3778,19 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3354,7 +3799,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3393,6 +3838,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3430,7 +3884,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", diff --git a/Cargo.toml b/Cargo.toml index 6cf113c..da8c429 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,11 @@ edition = "2024" base64 = "0.22.1" chrono = { version = "0.4.41", features = ["serde"] } rocket = { path = "../rocket/core/lib", features = ["json", "tls", "secrets"] } +rocket_dyn_templates = { path = "../rocket/contrib/dyn_templates", features = ["tera"] } rocket_db_pools = { path = "../rocket/contrib/db_pools/lib", features = ["sqlx_sqlite", "sqlx_macros"] } sqlx = "*" semver = { version = "1.0.26", features = ["serde"] } password-hash = { version = "0.5.0", features = ["std"] } argon2 = { version = "0.5.3", features = ["std"] } lazy_static = "1.5.0" +either = "1.15.0" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4c2a440 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM alpine +WORKDIR / + +COPY ./target/x86_64-unknown-linux-musl/release/gpodder-rs /gpodder-rs +COPY ./Rocket.toml /Rocket.toml + +VOLUME ["/data/"] +CMD ["/gpodder-rs"] +# ENTRYPOINT ["/bin/sh"] +EXPOSE 8000/tcp diff --git a/Rocket.toml b/Rocket.toml index b24d603..28f192a 100644 --- a/Rocket.toml +++ b/Rocket.toml @@ -1,14 +1,14 @@ [default] -log_level = "debug" +log_level = "info" address = "0.0.0.0" port = 8000 -[default.tls] -key = "certs/key.pem" -certs = "certs/cert.pem" +# [default.tls] +# key = "certs/key.pem" +# certs = "certs/cert.pem" [default.databases.podcast_db] -url = "podcasts.sqlite" +url = "data/podcasts.sqlite" [default.limits] json = "100MiB" diff --git a/src/admin.rs b/src/admin.rs new file mode 100644 index 0000000..b9a6e4d --- /dev/null +++ b/src/admin.rs @@ -0,0 +1,173 @@ +use std::convert::Infallible; + +use argon2::Argon2; +use either::Either; +use password_hash::{PasswordHasher, SaltString, rand_core::OsRng}; +use rocket::{ + FromForm, Request, Route, TypedError, async_trait, + form::Form, + get, + outcome::{Outcome, try_outcome}, + post, + request::FromRequest, + response::Redirect, + routes, + serde::{Deserialize, Serialize}, + trace::info, + uri, +}; +use rocket_db_pools::Connection; +use rocket_dyn_templates::{Template, context}; + +use crate::{ + Db, SqlError, + auth::{self, AdminUser, BasicAuth}, +}; + +#[get("/", rank = 1)] +fn dash(auth: Option) -> Template { + info!("user: {:?}", auth.as_ref().map(|a| a.username())); + Template::render( + "dash", + context! { + username: auth.as_ref().map(|a| a.username()), + admin: auth.as_ref().map(|a| a.is_admin()).unwrap_or(false), + mode: "dark", + }, + ) +} + +#[get("/login")] +fn login(auth: Option) -> Either { + if auth.is_some() { + Either::Right(Redirect::temporary(uri!(dash))) + } else { + Either::Left(Template::render( + "login", + context! { + mode: "dark", + }, + )) + } +} + +#[get("/account")] +fn account(auth: BasicAuth) -> Template { + Template::render( + "account", + context! { + username: auth.username(), + admin: auth.is_admin(), + mode: "dark", + }, + ) +} + +#[get("/admin")] +async fn admin(auth: AdminUser, mut db: Connection) -> Result { + let users = sqlx::query!("SELECT * from users") + .fetch_all(&mut **db) + .await?; + Ok(Template::render( + "admin", + context! { + username: auth.username(), + admin: auth.is_admin(), + users: users.into_iter().filter(|r| r.name != auth.username()).map(|r| r.name).collect::>(), + mode: "dark", + }, + )) +} + +struct InitialSetup; +#[derive(Debug, TypedError)] +pub enum InitialSetupError { + #[error(status = 500)] + InternalError, + #[error(status = 500)] + DbError(#[error(source)] SqlError), +} +impl From for InitialSetupError { + fn from(value: SqlError) -> Self { + Self::DbError(value) + } +} +impl From for InitialSetupError { + fn from(value: sqlx::Error) -> Self { + Self::DbError(value.into()) + } +} +#[derive(Debug, TypedError)] +struct Empty; +impl From for Empty { + fn from(value: Infallible) -> Self { + match value {} + } +} +#[async_trait] +impl<'r> FromRequest<'r> for InitialSetup { + type Forward = Empty; + type Error = InitialSetupError; + async fn from_request(req: &'r Request<'_>) -> Outcome { + // TODO: cache result in managed state? + let mut db = try_outcome!( + req.guard::>() + .await + .map_error(|_| InitialSetupError::InternalError) + ); + let user_count = try_outcome!( + sqlx::query!("SELECT count(name) as count from users") + .fetch_one(&mut **db) + .await + .into() + ); + if user_count.count == 0 { + Outcome::Success(Self) + } else { + Outcome::Forward(Empty) + } + } +} + +#[get("/")] +fn initial_setup(_initial: InitialSetup) -> Template { + Template::render("initial_setup", context! {}) +} + +#[derive(Debug, Serialize, Deserialize, FromForm)] +#[serde(crate = "rocket::serde")] +pub struct InitialSetupOptions<'a> { + admin_username: &'a str, + admin_password: &'a str, +} + +#[post("/initial_setup", data = "")] +async fn initial_complete( + _initial: InitialSetup, + opts: Form>, + mut db: Connection, +) -> Result { + let salt = SaltString::generate(&mut OsRng); + let pw_hash = Argon2::default() + .hash_password(opts.admin_password.as_bytes(), salt.as_salt()) + .expect("Failed to hash password"); + sqlx::query("INSERT INTO users (name, role, password) VALUES (?1, \"admin\", ?2)") + .bind(opts.admin_username) + .bind(pw_hash.to_string()) + .execute(&mut **db) + .await?; + Ok(Redirect::to("/")) +} + +pub fn routes() -> Vec { + routes![ + dash, + login, + auth::login_form, + auth::update_password, + account, + admin, + initial_setup, + initial_complete, + ] +} diff --git a/src/auth.rs b/src/auth.rs index a4c461e..dc52e20 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,24 +1,19 @@ -#![allow(private_interfaces)] use std::str::Utf8Error; use argon2::{Argon2, PasswordVerifier}; use base64::{DecodeError, Engine, alphabet::STANDARD, engine::GeneralPurposeConfig}; use password_hash::{PasswordHash, PasswordHasher, SaltString, rand_core::OsRng}; use rocket::{ - Request, Route, TypedError, async_trait, - http::{CookieJar, Status}, - post, - request::{FromRequest, Outcome}, - routes, - serde::{Deserialize, Serialize, json::Json}, - trace::debug, + async_trait, catch, catchers, form::Form, http::{CookieJar, Status}, post, request::{FromRequest, Outcome}, response::Redirect, routes, serde::{Deserialize, Serialize}, trace::debug, uri, Catcher, FromForm, Request, Route, TypedError }; use rocket_db_pools::Connection; +use rocket_dyn_templates::{context, Template}; use crate::{Db, SqlError}; pub struct BasicAuth { username: String, + is_admin: bool, } #[derive(Debug, TypedError)] pub enum Unauthorized { @@ -33,6 +28,8 @@ pub enum Unauthorized { #[error(status = 401)] UserNotFound, #[error(status = 401)] + UserNotAuthorized, + #[error(status = 401)] PasswordIncorrect, #[error(status = 500)] InternalError, @@ -55,14 +52,21 @@ impl BasicAuth { async fn from_req<'r>(req: &'r Request<'_>) -> Result { // TODO: actual sessions if let Some(cookie) = req.cookies().get_private("SESSION") { - return Ok(Self { - username: cookie.value().into(), - }); - // } else if let Some(username) = req.headers().get_one("test") { - // return Ok(Self { - // username: username.into(), - // }); + let raw = cookie.value(); + if let Some((role, username)) = raw.split_once(":") { + return Ok(Self { + username: username.into(), + is_admin: role == "admin" + }); + } else { + return Err(Unauthorized::InternalError); + } } + // else if let Some(username) = req.headers().get_one("test") { + // return Ok(Self { + // username: username.into() + // }) + // } let auth = req .headers() .get_one("Authorization") @@ -89,14 +93,13 @@ impl BasicAuth { .ok_or(Unauthorized::UserNotFound)?; let hashed = PasswordHash::new(&user.password).expect("Invalid password hash stored in the db"); - // hashed - // .verify_password(&[&Argon2::default()], pass) Argon2::default().verify_password(pass.as_bytes(), &hashed) .map_err(|_| Unauthorized::PasswordIncorrect) .map(|()| { - req.cookies().add_private(("SESSION", user.name.clone())); + req.cookies().add_private(("SESSION", format!("{}:{}", user.role, user.name))); Self { username: user.name, + is_admin: user.role == "admin" } }) } @@ -118,6 +121,35 @@ impl BasicAuth { pub fn username(&self) -> &str { &self.username } + // pub fn into_username(self) -> String { + // self.username + // } + + pub fn is_admin(&self) -> bool { + self.is_admin + }} + +pub struct AdminUser(BasicAuth); + +impl std::ops::Deref for AdminUser { + type Target = BasicAuth; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[async_trait] +impl<'r> FromRequest<'r> for AdminUser { + type Forward = std::convert::Infallible; + type Error = Unauthorized; + async fn from_request(req: &'r Request<'_>) -> Outcome { + match BasicAuth::from_req(req).await { + Ok(v) if v.is_admin => rocket::outcome::Outcome::Success(Self(v)), + Ok(_) => rocket::outcome::Outcome::Error(Unauthorized::UserNotAuthorized), + Err(v) => rocket::outcome::Outcome::Error(v), + } + } } #[post("/auth//login.json")] @@ -129,23 +161,22 @@ pub fn login(username: &str, auth: BasicAuth) -> Result<&'static str, Status> { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, FromForm)] #[serde(crate = "rocket::serde")] pub struct PasswordChange<'a> { password: &'a str, } -#[post("/auth/update_password", data = "")] +#[post("/update_password", data = "")] pub async fn update_password( auth: BasicAuth, - pw: Json>, + pw: Form>, mut db: Connection, ) -> Result<&'static str, SqlError> { let salt = SaltString::generate(&mut OsRng); let pw_hash = Argon2::default() .hash_password(pw.password.as_bytes(), salt.as_salt()) .expect("Failed to hash password"); - // pw_hash.to_string() sqlx::query("INSERT INTO users (name, password) VALUES (?1, ?2) ON CONFLICT DO UPDATE SET password = ?2") .bind(auth.username) .bind(pw_hash.to_string()) @@ -154,6 +185,38 @@ pub async fn update_password( Ok("") } +#[derive(Debug, Serialize, Deserialize, FromForm)] +#[serde(crate = "rocket::serde")] +pub struct LoginForm<'a> { + username: &'a str, + password: &'a str, +} + +#[post("/login", data = "")] +pub async fn login_form( + pw: Form>, + mut db: Connection, + cookies: &CookieJar<'_>, +) -> Result { + let user = sqlx::query!("SELECT * from users where name = ?", pw.username) + .fetch_optional(&mut **db) + .await? + .ok_or(Unauthorized::UserNotFound)?; + let hashed = + PasswordHash::new(&user.password).expect("Invalid password hash stored in the db"); + Argon2::default().verify_password(pw.password.as_bytes(), &hashed) + .map_err(|_| Unauthorized::PasswordIncorrect)?; + cookies.add_private(("SESSION", format!("{}:{}", user.role, pw.username))); + Ok(Redirect::to(uri!("/"))) +} + +#[catch(401, error="")] +pub fn auth_failure(e: &Unauthorized) -> Template { + Template::render("401", context! { + error: format!("{e:?}") + }) +} + #[post("/auth//logout.json")] pub fn logout( username: &str, @@ -169,5 +232,9 @@ pub fn logout( } pub fn routes() -> Vec { - routes![login, logout, update_password] + routes![login, logout] +} + +pub fn catchers() -> Vec { + catchers![auth_failure] } diff --git a/src/main.rs b/src/main.rs index 4033406..532f5d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#![allow(private_interfaces)] use rocket::{catch, catchers, fairing::AdHoc, launch, TypedError}; mod auth; @@ -7,14 +8,18 @@ mod subscriptions; mod suggestions; mod time; mod episodes; +mod admin; use rocket_db_pools::{ Database, sqlx, }; +use rocket_dyn_templates::Template; pub use time::Timestamp; mod format; pub use format::*; +use crate::auth::catchers; + #[derive(Debug, TypedError)] pub struct SqlError(sqlx::Error); @@ -34,8 +39,8 @@ fn catch_sql(error: &SqlError) -> String { struct Db(sqlx::SqlitePool); const SQL_INIT: &[&str] = &[ - "CREATE TABLE IF NOT EXISTS users (name TEXT PRIMARY KEY NOT NULL, password TEXT NOT NULL);", - "REPLACE INTO users (name, password) VALUES ('matt', 'pass')", + "CREATE TABLE IF NOT EXISTS users (name TEXT PRIMARY KEY NOT NULL, role TEXT NOT NULL, password TEXT NOT NULL);", + // "REPLACE INTO users (name, password) VALUES ('matt', 'admin', 'pass')", "CREATE TABLE IF NOT EXISTS devices ( id TEXT NOT NULL, user TEXT NOT NULL, @@ -77,6 +82,7 @@ const SQL_INIT: &[&str] = &[ fn launch() -> _ { rocket::build() .attach(Db::init()) + .attach(Template::fairing()) .attach(AdHoc::on_liftoff("Init db", |r| { Box::pin(async { if let Some(db) = Db::fetch(r) { @@ -98,5 +104,7 @@ fn launch() -> _ { // .mount("/", suggestions::routes()) // .mount("/api/2", suggestions::routes()) .mount("/api/2", episodes::routes()) + .mount("/", admin::routes()) .register("/", catchers![catch_sql]) + .register("/", catchers()) } diff --git a/templates/401.html.tera b/templates/401.html.tera new file mode 100644 index 0000000..38984ee --- /dev/null +++ b/templates/401.html.tera @@ -0,0 +1,7 @@ +{% extends "_layout" %} + +{% block content %} +
+ {{ error }} +
+{% endblock content %} diff --git a/templates/_basic.html.tera b/templates/_basic.html.tera new file mode 100644 index 0000000..847c5fc --- /dev/null +++ b/templates/_basic.html.tera @@ -0,0 +1,17 @@ + + + + {% block head %} + + {% block title %}{% endblock title %} + + {% endblock head %} + + + {% block body %} + +
{% block content %}{% endblock content %}
+ + {% endblock body %} + + diff --git a/templates/_layout.html.tera b/templates/_layout.html.tera new file mode 100644 index 0000000..b1f8085 --- /dev/null +++ b/templates/_layout.html.tera @@ -0,0 +1,48 @@ + + + + {% block head %} + + {% block title %}{% endblock title %} + + {##} + {% endblock head %} + + + {% block body %} + +
{% block content %}{% endblock %}
+ + + {% endblock body %} + + diff --git a/templates/account.html.tera b/templates/account.html.tera new file mode 100644 index 0000000..ed3be5a --- /dev/null +++ b/templates/account.html.tera @@ -0,0 +1,15 @@ +{% extends "_layout" %} + +{% block content %} +
+
+ +
+
+ +
+
+ +
+
+{% endblock content %} diff --git a/templates/admin.html.tera b/templates/admin.html.tera new file mode 100644 index 0000000..100d366 --- /dev/null +++ b/templates/admin.html.tera @@ -0,0 +1,49 @@ +{% extends "_layout" %} + +{% block content %} +
+

Create new user

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+

Edit user

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+{% endblock content %} diff --git a/templates/dash.html.tera b/templates/dash.html.tera new file mode 100644 index 0000000..1a13d8e --- /dev/null +++ b/templates/dash.html.tera @@ -0,0 +1,2 @@ +{% extends "_layout" %} + diff --git a/templates/initial_setup.html.tera b/templates/initial_setup.html.tera new file mode 100644 index 0000000..1396c59 --- /dev/null +++ b/templates/initial_setup.html.tera @@ -0,0 +1,15 @@ +{% extends "_layout" %} + +{% block content %} +
+
+ + +
+
+ + +
+ +
+{% endblock content %} diff --git a/templates/login.html.tera b/templates/login.html.tera new file mode 100644 index 0000000..2f797d2 --- /dev/null +++ b/templates/login.html.tera @@ -0,0 +1,15 @@ +{% extends "_layout" %} + +{% block content %} +
+
+ + +
+
+ + +
+ +
+{% endblock content %}