implement user pages + ui
All checks were successful
ci/woodpecker/push/build Pipeline was successful

This commit is contained in:
JMARyA 2025-01-12 03:58:16 +01:00
parent 67c31725c1
commit 3fabc91438
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
11 changed files with 1021 additions and 264 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target
/data

309
Cargo.lock generated
View file

@ -17,18 +17,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@ -132,9 +120,9 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.83"
version = "0.1.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
dependencies = [
"proc-macro2",
"quote",
@ -207,7 +195,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "based"
version = "0.1.0"
source = "git+https://git.hydrar.de/jmarya/based#04852f2fbcc301d0c2b4098f613b9450b4474363"
source = "git+https://git.hydrar.de/jmarya/based#37f65b6353ce1c87dbf71847d02bf787e025a680"
dependencies = [
"bcrypt",
"chrono",
@ -260,9 +248,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
dependencies = [
"serde",
]
@ -311,11 +299,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "cc"
version = "1.2.5"
name = "bytesize"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
[[package]]
name = "cc"
version = "1.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0cf6e91fde44c773c6ee7ec6bba798504641a8bc2eb7e37a04ffbf4dfaa55a"
dependencies = [
"jobserver",
"libc",
"shlex",
]
@ -532,7 +528,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.7.0",
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
@ -651,9 +647,9 @@ dependencies = [
[[package]]
name = "event-listener"
version = "5.3.1"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
dependencies = [
"concurrent-queue",
"parking",
@ -680,6 +676,18 @@ dependencies = [
"version_check",
]
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]]
name = "flume"
version = "0.11.1"
@ -697,6 +705,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
name = "foreign-types"
version = "0.3.2"
@ -863,9 +877,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "glob"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "h2"
@ -891,24 +905,25 @@ name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "hashlink"
version = "0.9.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.14.5",
"hashbrown 0.15.2",
]
[[package]]
@ -1267,6 +1282,15 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.76"
@ -1298,22 +1322,32 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.7.0",
"libc",
"redox_syscall",
]
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "litemap"
@ -1405,12 +1439,6 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.2"
@ -1467,16 +1495,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -1571,7 +1589,7 @@ version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.7.0",
"cfg-if",
"foreign-types",
"libc",
@ -1620,11 +1638,16 @@ name = "pacco"
version = "0.1.0"
dependencies = [
"based",
"bytesize",
"chrono",
"env_logger 0.11.6",
"log",
"maud",
"rocket",
"serde_json",
"sqlx",
"tar",
"zstd",
]
[[package]]
@ -1656,12 +1679,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pear"
version = "0.2.9"
@ -1702,9 +1719,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project-lite"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
@ -1779,9 +1796,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.92"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
@ -1864,7 +1881,7 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.7.0",
]
[[package]]
@ -2113,11 +2130,11 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
version = "0.38.42"
version = "0.38.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.7.0",
"errno",
"libc",
"linux-raw-sys",
@ -2181,7 +2198,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.7.0",
"core-foundation",
"core-foundation-sys",
"libc",
@ -2190,9 +2207,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.13.0"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
@ -2200,18 +2217,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.216"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.216"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@ -2220,9 +2237,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.134"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
dependencies = [
"itoa",
"memchr",
@ -2360,21 +2377,11 @@ dependencies = [
"der",
]
[[package]]
name = "sqlformat"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
dependencies = [
"nom",
"unicode_categories",
]
[[package]]
name = "sqlx"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e"
checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
dependencies = [
"sqlx-core",
"sqlx-macros",
@ -2385,38 +2392,32 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e"
checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
dependencies = [
"atoi",
"byteorder",
"bytes",
"chrono",
"crc",
"crossbeam-queue",
"either",
"event-listener",
"futures-channel",
"futures-core",
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.14.5",
"hashbrown 0.15.2",
"hashlink",
"hex",
"indexmap",
"log",
"memchr",
"native-tls",
"once_cell",
"paste",
"percent-encoding",
"serde",
"serde_json",
"sha2",
"smallvec",
"sqlformat",
"thiserror",
"tokio",
"tokio-stream",
@ -2427,9 +2428,9 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657"
checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
dependencies = [
"proc-macro2",
"quote",
@ -2440,9 +2441,9 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5"
checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
dependencies = [
"dotenvy",
"either",
@ -2466,13 +2467,13 @@ dependencies = [
[[package]]
name = "sqlx-mysql"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a"
checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
dependencies = [
"atoi",
"base64 0.22.1",
"bitflags 2.6.0",
"bitflags 2.7.0",
"byteorder",
"bytes",
"chrono",
@ -2510,13 +2511,13 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8"
checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
dependencies = [
"atoi",
"base64 0.22.1",
"bitflags 2.6.0",
"bitflags 2.7.0",
"byteorder",
"chrono",
"crc",
@ -2524,7 +2525,6 @@ dependencies = [
"etcetera",
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"hex",
"hkdf",
@ -2550,9 +2550,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680"
checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
dependencies = [
"atoi",
"chrono",
@ -2616,9 +2616,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.92"
version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ae51629bf965c5c098cc9e87908a3df5301051a9e087d6f9bef5c9771ed126"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [
"proc-macro2",
"quote",
@ -2664,13 +2664,25 @@ dependencies = [
]
[[package]]
name = "tempfile"
version = "3.14.0"
name = "tar"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "tempfile"
version = "3.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
dependencies = [
"cfg-if",
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys 0.59.0",
@ -2687,18 +2699,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.69"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
@ -2773,9 +2785,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.42.0"
version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [
"backtrace",
"bytes",
@ -2791,9 +2803,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
@ -3016,12 +3028,6 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unicode_categories"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "untrusted"
version = "0.7.1"
@ -3059,9 +3065,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4"
dependencies = [
"getrandom",
"serde",
@ -3402,9 +3408,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.20"
version = "0.6.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
dependencies = [
"memchr",
]
@ -3431,6 +3437,17 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "xattr"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909"
dependencies = [
"libc",
"linux-raw-sys",
"rustix",
]
[[package]]
name = "yansi"
version = "1.0.1"
@ -3533,3 +3550,31 @@ dependencies = [
"quote",
"syn",
]
[[package]]
name = "zstd"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.13+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
dependencies = [
"cc",
"pkg-config",
]

View file

@ -5,8 +5,13 @@ edition = "2024"
[dependencies]
based = { git = "https://git.hydrar.de/jmarya/based", features = ["htmx"]}
bytesize = "1.3.0"
chrono = "0.4.39"
env_logger = "0.11.6"
log = "0.4.22"
maud = "0.26.0"
rocket = "0.5.1"
serde_json = "1.0.134"
sqlx = "0.8.2"
tar = "0.4.43"
zstd = "0.13.2"

View file

@ -16,14 +16,6 @@ To use the packages pacco provides, add the following to `pacman.conf`:
# /etc/pacman.conf
[repo]
Include = /etc/pacman.d/mirrorlist_pacco
```
Add `/etc/pacman.d/mirrorlist_pacco`:
```
# /etc/pacman.d/mirrorlist_pacco
Server = https://example.com/pkg/$repo/$arch
```

View file

@ -1,40 +1,9 @@
// TODO :
// - Base
// - API
// - UI
// - PkgDB Abstraction
// - Pkg Abstraction
use based::auth::User;
use based::get_pg;
use based::page::{Shell, render_page};
use based::request::{RequestContext, StringResponse};
use maud::html;
use pacco::pkg::Repository;
use rocket::get;
use rocket::routes;
pub mod routes;
#[get("/")]
pub async fn index_page(ctx: RequestContext) -> StringResponse {
let repos: Vec<String> = Repository::list();
let content = html!(
h1 { "Repositories" };
@for repo in repos {
p { (repo) };
};
);
render_page(
content,
"Repositories",
ctx,
&Shell::new(html! {}, html! {}, Some(String::new())),
)
.await
}
#[rocket::launch]
async fn launch() -> _ {
env_logger::init();
@ -42,9 +11,21 @@ async fn launch() -> _ {
let pg = get_pg!();
sqlx::migrate!("./migrations").run(pg).await.unwrap();
let _ = User::create("admin".to_string(), "admin", based::auth::UserRole::Admin).await;
rocket::build().mount("/", routes![
index_page,
based::htmx::htmx_script_route,
routes::index_page,
routes::pkg_route,
routes::upload_pkg
routes::push::upload_pkg,
routes::user::login,
routes::user::login_post,
routes::user::account_page,
routes::ui::pkg_ui,
routes::ui::repo_ui,
routes::user::new_api_key,
routes::user::end_session,
routes::user::change_password,
routes::user::change_password_post
])
}

View file

@ -1,4 +1,10 @@
use std::path::{Path, PathBuf};
use std::{
fs::File,
io::{Cursor, Read},
path::{Path, PathBuf},
};
use tar::Archive;
use super::{Repository, arch::Architecture};
@ -6,11 +12,11 @@ use super::{Repository, arch::Architecture};
#[derive(Debug, Clone)]
pub struct Package {
/// Repository of the package
repo: String,
pub repo: String,
/// `Architecture` of the package
arch: Architecture,
pub arch: Architecture,
/// Name of the package
name: String,
pub name: String,
/// Version of the package
version: Option<String>,
}
@ -20,12 +26,78 @@ impl Package {
pub fn new(repo: &str, arch: Architecture, pkg_name: &str, version: &str) -> Self {
Package {
repo: repo.to_string(),
arch: arch,
arch,
name: pkg_name.to_string(),
version: Some(version.to_string()),
}
}
pub fn install_script(&self) -> Option<String> {
let pkg = self.base_path().join(self.file_name());
read_file_tar(&pkg, ".INSTALL")
}
pub fn file_list(&self) -> Vec<String> {
list_tar_file(&self.base_path().join(self.file_name())).unwrap()
}
pub fn binaries(&self) -> Vec<String> {
list_tar_file(&self.base_path().join(self.file_name()))
.unwrap_or_default()
.into_iter()
.filter_map(|x| {
let mut paths: Vec<_> = x.split("/").collect();
paths.reverse();
let parent = paths.get(1)?;
if (*parent == "bin" || *parent == "sbin") && !x.ends_with("/") {
return Some(x);
}
None
})
.collect()
}
pub fn pkginfo(&self) -> Vec<(String, String)> {
let content = read_file_tar(&self.base_path().join(self.file_name()), ".PKGINFO").unwrap();
let mut ret: Vec<(String, Vec<String>)> = Vec::new();
for line in content.split("\n") {
if line.starts_with('#') || line.is_empty() {
continue;
}
let (key, val) = line.split_once(" = ").unwrap();
if let Some(e) = ret.iter_mut().find(|x| x.0 == key) {
e.1.push(val.to_string());
} else {
ret.push((key.to_string(), vec![val.to_string()]));
}
}
let mut ret: Vec<_> = ret.into_iter().map(|x| (x.0, x.1.join(" "))).collect();
ret.sort_by(|a, b| a.0.cmp(&b.0));
ret
}
pub fn arch(&self) -> Vec<Architecture> {
let mut ret = Vec::new();
for a in [
Architecture::x86_64,
Architecture::aarch64,
Architecture::any,
] {
let check_pkg = self.switch_arch(a.clone());
if check_pkg.exists() {
ret.push(a);
}
}
ret
}
/// Extract values from a package filename
///
/// # Example
@ -47,10 +119,12 @@ impl Package {
/// ```
pub fn extract_pkg_name(file_name: &str) -> Option<(String, String, String, Architecture)> {
// Extract (assuming the filename is "<pkg_name>-<version>-<relation>-<arch>.pkg.tar.zst")
let file_name = file_name.trim_end_matches(".sig").to_string();
let mut splitted = file_name.split('-').collect::<Vec<&str>>();
let arch = splitted.pop()?;
assert!(arch.ends_with(".pkg.tar.zst"));
assert!(arch.ends_with(".pkg.tar.zst"), "{file_name}");
let arch = arch.trim_end_matches(".pkg.tar.zst");
let relation = splitted.pop()?;
@ -58,12 +132,12 @@ impl Package {
let pkg_name = splitted.join("-");
return Some((
Some((
pkg_name,
version.to_string(),
relation.to_string(),
Architecture::parse(arch)?,
));
))
}
/// Parse a pkg filename
@ -78,25 +152,52 @@ impl Package {
}
/// Find a package with latest version
pub fn find(repo: &str, arch: &str, pkg_name: &str) -> Self {
pub fn find(repo: &str, arch: Architecture, pkg_name: &str) -> Option<Self> {
let mut base = Package {
repo: repo.to_string(),
arch: Architecture::parse(arch).unwrap(),
arch,
name: pkg_name.to_string(),
version: None,
};
let versions = base.versions();
let ver = versions.first().unwrap();
let ver = versions.first()?;
base.version = Some(ver.clone());
base
Some(base)
}
pub fn systemd_units(&self) -> Vec<String> {
// TODO : Extract unit infos
list_tar_file(&self.base_path().join(self.file_name()))
.unwrap_or_default()
.into_iter()
.filter(|x| {
let ext = x.split(".").last().unwrap();
ext == "service" || ext == "timer" || ext == "mount"
})
.collect()
}
pub fn pacman_hooks(&self) -> Vec<(String, String)> {
let pkg_file = self.base_path().join(self.file_name());
let files = list_tar_file(&pkg_file).unwrap_or_default();
files
.into_iter()
.filter(|x| {
x.starts_with("etc/pacman.d/hooks/") || x.starts_with("usr/share/libalpm/hooks/")
})
.map(|x| {
let content = read_file_tar(&pkg_file, &x).unwrap();
(x, content)
})
.collect()
}
/// Save a new package to repository
pub fn save(&self, pkg: Vec<u8>, sig: Option<Vec<u8>>) {
let pkg_file = self.base_path().join(&self.file_name());
let pkg_file = self.base_path().join(self.file_name());
let sig_file = self.base_path().join(format!("{}.sig", self.file_name()));
std::fs::write(&pkg_file, pkg).unwrap();
@ -106,7 +207,7 @@ impl Package {
let db_file = PathBuf::from("./data")
.join(&self.repo)
.join(&self.arch.to_string())
.join(self.arch.to_string())
.join(format!("{}.db.tar.gz", self.repo));
repo_add(db_file.to_str().unwrap(), pkg_file.to_str().unwrap());
@ -122,7 +223,7 @@ impl Package {
let db_file = PathBuf::from("./data")
.join(&self.repo)
.join(&arch.to_string())
.join(arch.to_string())
.join(format!("{}.db.tar.gz", self.repo));
repo_add(db_file.to_str().unwrap(), pkg_file.to_str().unwrap());
@ -134,16 +235,16 @@ impl Package {
// <repo>/<arch>/<pkg>/
let p = Path::new("./data")
.join(&self.repo)
.join(&self.arch.to_string())
.join(self.arch.to_string())
.join(&self.name);
std::fs::create_dir_all(&p).unwrap();
p
}
/// Switch the `Architecture` of the package
pub fn switch_arch(&self, arch: &str) -> Self {
pub fn switch_arch(&self, arch: Architecture) -> Self {
let mut new = self.clone();
new.arch = Architecture::parse(arch).unwrap();
new.arch = arch;
new
}
@ -236,3 +337,44 @@ pub fn run_command(cmd: Vec<&str>) {
pub fn repo_add(db_file: &str, pkg_file: &str) {
run_command(vec!["repo-add", db_file, pkg_file]);
}
pub fn read_file_tar(tar: &PathBuf, file_path: &str) -> Option<String> {
let mut file = File::open(tar).ok()?;
let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap();
let uncompressed = zstd::decode_all(Cursor::new(buf)).unwrap();
let content = Cursor::new(uncompressed);
let mut a = Archive::new(content);
for e in a.entries().ok()? {
let mut e = e.ok()?;
let path = e.path().unwrap();
let path = path.to_str().unwrap();
if path == file_path {
let mut file_content = Vec::new();
e.read_to_end(&mut file_content).unwrap();
return String::from_utf8(file_content).ok();
}
}
None
}
pub fn list_tar_file(tar: &PathBuf) -> Option<Vec<String>> {
let mut file = File::open(tar).ok()?;
let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap();
let uncompressed = zstd::decode_all(Cursor::new(buf)).unwrap();
let content = Cursor::new(uncompressed);
let mut a = Archive::new(content);
let mut paths = Vec::new();
for e in a.entries().ok()? {
let e = e.ok()?;
let path = e.path().ok()?;
let path = path.to_str()?;
paths.push(path.to_string());
}
Some(paths)
}

View file

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::{collections::HashSet, path::PathBuf};
use super::{Package, arch::Architecture};
@ -68,7 +68,7 @@ impl Repository {
pub fn base_path(&self, arch: Architecture) -> PathBuf {
PathBuf::from("./data")
.join(&self.name)
.join(&arch.to_string())
.join(arch.to_string())
}
/// Get the `.db.tar.gz` content for the repository of `arch`
@ -89,23 +89,42 @@ impl Repository {
.ok()
}
pub fn get_pkg(&self, pkg_name: &str) -> Option<Package> {
// Normalize name
let pkg_name = if pkg_name.ends_with(".sig") {
pkg_name.trim_end_matches(".sig").to_string()
} else {
pkg_name.to_string()
};
pub fn list_pkg(&self) -> Vec<String> {
let mut packages = HashSet::new();
for arch in self.arch() {
for entry in std::fs::read_dir(self.base_path(arch)).unwrap().flatten() {
let path = entry.path();
let file_name = path.file_name().unwrap().to_str().unwrap().to_string();
if entry.metadata().unwrap().is_dir() {
packages.insert(file_name);
}
}
}
let mut pkg: Vec<_> = packages.into_iter().collect();
pkg.sort();
pkg
}
pub fn get_pkg_by_name(&self, pkg_name: &str) -> Option<Package> {
for arch in self.arch() {
if let Some(pkg) = Package::find(&self.name, arch, pkg_name) {
return Some(pkg);
}
}
None
}
pub fn get_pkg(&self, pkg_name: &str) -> Option<Package> {
// Find package
let (name, version, _, arch) = Package::extract_pkg_name(&pkg_name).unwrap();
let (name, version, _, arch) = Package::extract_pkg_name(pkg_name).unwrap();
let pkg = Package::new(&self.name, arch, &name, &version);
// Return if exists
if pkg.exists() {
return Some(pkg);
} else {
None
}
if pkg.exists() { Some(pkg) } else { None }
}
}

View file

@ -1,71 +1,44 @@
use based::request::api::{FallibleApiResponse, api_error};
use based::request::{RawResponse, RequestContext, respond_with};
use based::auth::MaybeUser;
use based::page::{Shell, htmx_link, render_page};
use based::request::{RawResponse, RequestContext, StringResponse, respond_with};
use maud::{PreEscaped, html};
use rocket::get;
use rocket::http::{ContentType, Status};
use rocket::tokio::io::AsyncReadExt;
use rocket::{FromForm, get, post};
use serde_json::json;
use pacco::pkg::Repository;
use pacco::pkg::arch::Architecture;
use pacco::pkg::{Package, Repository};
// /pkg/<repo>/<arch>/<pkg_name>
// /pkg/<repo>/<arch>/
pub mod push;
pub mod ui;
pub mod user;
use rocket::form::Form;
use rocket::fs::TempFile;
#[get("/")]
pub async fn index_page(ctx: RequestContext, user: MaybeUser) -> StringResponse {
let repos: Vec<String> = Repository::list();
#[derive(FromForm)]
pub struct PkgUpload<'r> {
name: String,
arch: String,
version: String,
pkg: TempFile<'r>,
sig: Option<TempFile<'r>>,
}
pub async fn tmp_file_to_vec<'r>(tmp: &TempFile<'r>) -> Vec<u8> {
let mut buf = Vec::with_capacity(tmp.len() as usize);
tmp.open()
.await
.unwrap()
.read_to_end(&mut buf)
.await
.unwrap();
buf
}
#[post("/pkg/<repo>/upload", data = "<upload>")]
pub async fn upload_pkg(
repo: &str,
upload: Form<PkgUpload<'_>>,
user: based::auth::APIUser,
) -> FallibleApiResponse {
// TODO : Permission System
if !user.0.is_admin() {
return Err(api_error("Forbidden"));
}
let pkg = Package::new(
repo,
Architecture::parse(&upload.arch).ok_or_else(|| api_error("Invalid architecture"))?,
&upload.name,
&upload.version,
let content = html!(
div class="flex justify-between" {
h1 class="text-4xl font-bold pb-6" { "Repositories" };
@if let Some(user) = user.take_user() {
a class="text-lg" href="/account" { (user.username) };
};
};
div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" {
@for repo in repos {
(htmx_link(&format!("/{repo}"), "flex items-center gap-4 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg shadow-md transition hover:bg-gray-200 dark:hover:bg-gray-600", "", html! {
p class="font-medium flex-1 text-gray-800 dark:text-gray-100" {
(repo)
};
}))
}
};
);
pkg.save(
tmp_file_to_vec(&upload.pkg).await,
if let Some(sig) = &upload.sig {
Some(tmp_file_to_vec(sig).await)
} else {
None
},
);
Ok(json!({"ok": format!("Added '{}' to '{}'", pkg.file_name(), repo)}))
render(content, "Repositories", ctx).await
}
#[get("/pkg/<repo>/<arch>/<pkg_name>")]
pub async fn pkg_route(repo: &str, arch: &str, pkg_name: &str, ctx: RequestContext) -> RawResponse {
pub async fn pkg_route(repo: &str, arch: &str, pkg_name: &str) -> RawResponse {
let arch = Architecture::parse(arch).unwrap();
if let Some(repo) = Repository::new(repo) {
@ -112,3 +85,25 @@ pub async fn pkg_route(repo: &str, arch: &str, pkg_name: &str, ctx: RequestConte
"Not found".as_bytes().to_vec(),
)
}
pub async fn render(
content: PreEscaped<String>,
title: &str,
ctx: RequestContext,
) -> StringResponse {
render_page(
content,
title,
ctx,
&Shell::new(
html! {
script src="https://cdn.tailwindcss.com" {};
script src="/assets/htmx.min.js" {};
meta name="viewport" content="width=device-width, initial-scale=1.0";
},
html! {},
Some("bg-gray-900 text-gray-200 min-h-screen p-10".to_string()),
),
)
.await
}

63
src/routes/push.rs Normal file
View file

@ -0,0 +1,63 @@
use based::request::api::{FallibleApiResponse, api_error};
use rocket::tokio::io::AsyncReadExt;
use rocket::{FromForm, post};
use serde_json::json;
use pacco::pkg::Package;
use pacco::pkg::arch::Architecture;
// /pkg/<repo>/<arch>/<pkg_name>
// /pkg/<repo>/<arch>/
use rocket::form::Form;
use rocket::fs::TempFile;
#[derive(FromForm)]
pub struct PkgUpload<'r> {
name: String,
arch: String,
version: String,
pkg: TempFile<'r>,
sig: Option<TempFile<'r>>,
}
pub async fn tmp_file_to_vec<'r>(tmp: &TempFile<'r>) -> Vec<u8> {
let mut buf = Vec::with_capacity(tmp.len() as usize);
tmp.open()
.await
.unwrap()
.read_to_end(&mut buf)
.await
.unwrap();
buf
}
#[post("/pkg/<repo>/upload", data = "<upload>")]
pub async fn upload_pkg(
repo: &str,
upload: Form<PkgUpload<'_>>,
user: based::auth::APIUser,
) -> FallibleApiResponse {
// TODO : Permission System
if !user.0.is_admin() {
return Err(api_error("Forbidden"));
}
let pkg = Package::new(
repo,
Architecture::parse(&upload.arch).ok_or_else(|| api_error("Invalid architecture"))?,
&upload.name,
&upload.version,
);
pkg.save(
tmp_file_to_vec(&upload.pkg).await,
if let Some(sig) = &upload.sig {
Some(tmp_file_to_vec(sig).await)
} else {
None
},
);
Ok(json!({"ok": format!("Added '{}' to '{}'", pkg.file_name(), repo)}))
}

292
src/routes/ui.rs Normal file
View file

@ -0,0 +1,292 @@
use based::{
page::htmx_link,
request::{RequestContext, StringResponse},
};
use maud::{PreEscaped, html};
use rocket::get;
use pacco::pkg::Repository;
use super::render;
// TODO : API
#[get("/<repo>/<pkg_name>")]
pub async fn pkg_ui(repo: &str, pkg_name: &str, ctx: RequestContext) -> Option<StringResponse> {
// TODO : Implement pkg UI
// pkgmeta display
let repo = Repository::new(repo).unwrap();
let pkg = repo.get_pkg_by_name(pkg_name)?;
let versions = pkg.versions();
let arch = pkg.arch();
let install_script = pkg.install_script();
let systemd_units = pkg.systemd_units();
let pacman_hooks = pkg.pacman_hooks();
let binaries = pkg.binaries();
let mut pkginfo = pkg.pkginfo();
let content = html! {
// Package Name
div class="flex flex-wrap gap-2 justify-center items-center" {
(htmx_link(&format!("/{}", repo.name), "text-3xl font-bold text-gray-800 dark:text-gray-100", "", html! {
(repo.name)
}))
p class="font-bold p-2 text-gray-400" { "/" };
h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100 pr-2" {
(pkg.name)
};
@if pkg.is_signed() {
div class="flex items-center gap-2 text-slate-300 pr-4" {
span class="text-2xl font-bold" {
""
}
span class="text-sm font-medium" { "Signed" }
// TODO : Add more info: Who signed? + Public Key recv
}
}
@for arch in arch {
span class="px-3 py-1 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full" {
(arch.to_string())
};
}
a href=(format!("/pkg/{}/{}/{}", pkg.repo, pkg.arch.to_string(), pkg.file_name())) class="ml-4 inline-flex items-center px-2 py-2 bg-gray-200 text-black font-xs rounded-lg shadow-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" {
svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" {
path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m7-7l-7 7-7-7" {};
};
p class="ml-2" { "Download" };
};
};
div class="flex pt-6" {
div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition flex-1 mr-4" {
h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300 underline" { "Info" };
@if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "pkgdesc" }).1 {
(build_info(desc.0, desc.1))
}
@if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "packager" }).1 {
(build_info(desc.0, desc.1))
}
@if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "url" }).1 {
(build_info(desc.0, desc.1))
}
@if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "size" }).1 {
(build_info(desc.0, desc.1))
}
@for (key, val) in pkginfo {
(build_info(key, val))
};
};
div class="space-y-2 max-w-80 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition" {
h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300" { "Versions" }
ul class="space-y-1" {
@for version in versions {
// TODO : Implement page per version ?version=
li class="text-gray-800 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400 transition" {
(version)
};
}
}
}
};
div class="flex flex-wrap pt-6" {
div class="space-y-2" {
h2 class="text-xl font-bold text-gray-700 dark:text-gray-300" { "Content" }
div class="flex" {
@if !systemd_units.is_empty() {
div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition mr-4" {
h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Systemd Units" }
ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" {
@for unit in systemd_units {
li { (unit) }
}
}
}
}
@if !pacman_hooks.is_empty() {
div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition mr-4" {
h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Pacman Hooks" }
ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" {
@for hook in pacman_hooks {
h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300" { (hook.0) }
pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg text-gray-800 dark:text-gray-100 overflow-x-auto text-sm" {
(hook.1)
}
}
}
}
}
@if !binaries.is_empty() {
div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition mr-4" {
h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Binaries" }
ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" {
@for binary in binaries {
li { (binary) }
}
}
}
}
div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition mr-4" {
h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Package Files" }
ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" {
@for file in pkg.file_list() {
li { (file) }
}
}
}
}
};
};
// Install Scripts
@if let Some(install_script) = install_script {
div class="space-y-4 pt-6" {
h2 class="text-3xl font-semibold text-gray-700 dark:text-gray-300" { "Install Scripts" }
pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg text-gray-800 dark:text-gray-100 overflow-x-auto text-sm" {
(install_script)
}
};
}
};
Some(render(content, pkg_name, ctx).await)
}
#[get("/<repo>")]
pub async fn repo_ui(repo: &str, ctx: RequestContext) -> StringResponse {
// TODO : Repo UI
// permissions
// pkg list
let repo = Repository::new(repo).unwrap();
let architectures: Vec<_> = repo.arch().into_iter().map(|x| x.to_string()).collect();
let packages = repo.list_pkg();
let content = html! {
// Repository name and architectures
div class="flex flex-wrap items-center justify-center mb-6" {
h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100 pr-4" {
(repo.name)
};
div class="flex gap-2 mt-2 md:mt-0" {
@for arch in architectures {
span class="px-3 py-1 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full" {
(arch)
};
}
}
};
// Package list
ul class="space-y-4" {
@for pkg in packages {
(htmx_link(&format!("/{}/{pkg}", repo.name), "flex items-center gap-4 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg shadow-md transition hover:bg-gray-200 dark:hover:bg-gray-600", "", html! {
div class="w-10 h-10 flex items-center justify-center rounded-full bg-blue-500 text-white font-semibold" {
{(pkg.chars().next().unwrap_or_default().to_uppercase())}
};
p class="font-medium flex-1 text-gray-800 dark:text-gray-100" {
(pkg)
};
}))
}
}
};
render(content, &repo.name, ctx).await
}
pub fn build_info(key: String, value: String) -> PreEscaped<String> {
match key.as_str() {
"pkgname" => {}
"xdata" => {}
"arch" => {}
"pkbase" => {}
"pkgver" => {}
"pkgdesc" => {
return key_value("Description".to_string(), value);
}
"packager" => {
return key_value("Packager".to_string(), value);
}
"url" => {
return html! {
div class="flex" {
span class="font-bold w-32" { (format!("Website: ")) };
a class="ml-2 text-blue-400" href=(value) { (value) };
};
};
}
"builddate" => {
let date = chrono::DateTime::from_timestamp(value.parse().unwrap(), 0).unwrap();
return key_value(
"Build at".to_string(),
date.format("%d.%m.%Y %H:%M").to_string(),
);
}
"depend" => {
// TODO : Find package link
return key_value("Depends on".to_string(), value);
}
"makedepend" => {
// TODO : Find package link
return key_value("Build Dependencies".to_string(), value);
}
"license" => {
return key_value("License".to_string(), value);
}
"size" => {
return key_value(
"Size".to_string(),
bytesize::to_string(value.parse().unwrap(), false),
);
}
_ => {
log::warn!("Unhandled PKGINFO {key} = {value}");
}
}
html! {}
}
pub fn key_value(key: String, value: String) -> PreEscaped<String> {
html! {
div class="flex" {
span class="font-bold w-32" { (format!("{key}: ")) };
span class="ml-2" { (value) };
};
}
}
pub fn take_out<T>(v: &mut Vec<T>, f: impl Fn(&T) -> bool) -> (&mut Vec<T>, Option<T>) {
let mut index = -1;
for (i, e) in v.iter().enumerate() {
if f(e) {
index = i as i64;
}
}
if index != -1 {
let e = v.remove(index as usize);
return (v, Some(e));
}
(v, None)
}

222
src/routes/user.rs Normal file
View file

@ -0,0 +1,222 @@
use based::{
auth::{Session, Sessions, User, csrf::CSRF},
page::htmx_link,
request::{RequestContext, StringResponse, api::to_uuid, respond_html},
};
use maud::{PreEscaped, html};
use rocket::{
FromForm,
form::Form,
get,
http::{Cookie, CookieJar},
post,
response::Redirect,
};
use super::render;
#[get("/login")]
pub async fn login(ctx: RequestContext) -> StringResponse {
let content = html! {
div class="min-w-screen justify-center flex items-center" {
div class="bg-gray-800 shadow-lg rounded-lg p-8 max-w-sm w-full" {
h2 class="text-2xl font-bold text-center text-gray-100 mb-6" { "Login" };
form action="/login" method="POST" {
div class="mb-4" {
label for="email" class="block text-sm font-medium text-gray-400" { "Email" };
input type="text" id="username" name="username" placeholder="Username" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {};
};
div class="mb-4" {
label for="password" class="block text-sm font-medium text-gray-400" { "Password" };
input type="password" id="password" name="password" placeholder="Password" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {};
};
button type="submit" class="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400" { "Login" };
};
};
};
};
render(content, "Login", ctx).await
}
#[derive(FromForm)]
pub struct LoginForm {
username: String,
password: String,
}
#[post("/login", data = "<form>")]
pub async fn login_post(form: Form<LoginForm>, cookies: &CookieJar<'_>) -> Redirect {
if let Some(user) = User::login(&form.username, &form.password).await {
let session_cookie = Cookie::build(("session", user.0.token.to_string()))
.path("/")
.http_only(true)
.max_age(rocket::time::Duration::days(7))
.build();
cookies.add(session_cookie);
Redirect::to("/")
} else {
Redirect::to("/login")
}
}
#[get("/end_session/<session_id>?<csrf>")]
pub async fn end_session(session_id: &str, csrf: &str, user: User) -> StringResponse {
if user.verify_csrf(csrf).await {
user.end_session(&to_uuid(session_id).unwrap()).await;
respond_html(
html! {
(user.update_csrf().await)
}
.into_string(),
)
} else {
respond_html(html! { p { "Invalid CSRF" }}.into_string())
}
}
#[get("/new_api_key?<csrf>&<session_name>")]
pub async fn new_api_key(user: User, csrf: &str, session_name: &str) -> StringResponse {
if user.verify_csrf(csrf).await {
if session_name.is_empty() {
return respond_html(
html! {
div id="next_session" {};
(user.update_csrf().await)
}
.into_string(),
);
}
let api = user.api_key(session_name).await;
respond_html(
html! {
li class="justify-between items-center bg-gray-50 p-4 rounded shadow" {
span class="text-gray-700" { (api.name.unwrap_or_default()) };
br {};
span class="text-red-500" { (api.token) };
};
div id="next_session" {};
(user.update_csrf().await)
}
.into_string(),
)
} else {
respond_html(
html! {
p { "CSRF!" };
}
.into_string(),
)
}
}
#[get("/account")]
pub async fn account_page(user: User, ctx: RequestContext) -> StringResponse {
let sessions = user.list_sessions().await;
let content = html! {
main class="w-full mt-6 rounded-lg shadow-md p-6" {
section class="mb-6" {
h2 class="text-xl font-semibold mb-2" { (user.username) };
};
(htmx_link("/passwd", "mb-6 bg-green-500 text-white py-2 px-6 rounded hover:bg-green-600", "", html! { "Change Password" }))
section class="mb-6 mt-6" {
h3 class="text-lg font-semibold mb-4" { "Active Sessions" };
ul class="space-y-4" id="sessions-list" {
@for ses in sessions {
(build_session_block(&ses, &user).await)
};
div id="next_session" {};
}
}
form class="flex" hx-get="/new_api_key" hx-target="#next_session"
hx-swap="outerHTML" {
input type="text" name="session_name" placeholder="API Key Name" class="text-black bg-gray-100 px-4 py-2 rounded mr-4" {};
input type="hidden" class="csrf" name="csrf" value=(user.get_csrf().await) {};
button class="bg-green-500 text-white py-2 px-6 rounded hover:bg-green-600" { "New API Key" };
}
};
};
render(content, "Account", ctx).await
}
pub async fn build_session_block(ses: &Session, user: &User) -> PreEscaped<String> {
html! {
li class="flex justify-between items-center bg-gray-50 p-4 rounded shadow" {
span class="text-gray-700" { (ses.name.clone().unwrap_or("Session".to_string())) };
form hx-target="closest li" hx-swap="outerHTML" hx-get=(format!("/end_session/{}", ses.id)) {
input type="hidden" class="csrf" name="csrf" value=(user.get_csrf().await) {};
button class="bg-red-500 text-white py-1 px-4 rounded hover:bg-red-600"
{ "Kill" };
};
}
}
}
#[derive(FromForm)]
pub struct PasswordChangeForm {
password_old: String,
password_new: String,
password_new_repeat: String,
csrf: String,
}
// TODO : Change password pages
#[post("/passwd", data = "<form>")]
pub async fn change_password_post(form: Form<PasswordChangeForm>, user: User) -> Redirect {
if form.password_new != form.password_new_repeat {
return Redirect::to("/passwd");
}
if !user.verify_csrf(&form.csrf).await {
return Redirect::to("/passwd");
}
user.passwd(&form.password_old, &form.password_new)
.await
.unwrap();
Redirect::to("/account")
}
#[get("/passwd")]
pub async fn change_password(ctx: RequestContext, user: User) -> StringResponse {
let content = html! {
div class="min-w-screen justify-center flex items-center" {
div class="bg-gray-800 shadow-lg rounded-lg p-8 max-w-sm w-full" {
h2 class="text-2xl font-bold text-center text-gray-100 mb-6" { "Change Password" };
form action="/passwd" method="POST" {
div class="mb-4" {
label for="password_old" class="block text-sm font-medium text-gray-400" { "Old Password" };
input type="password" id="password_old" name="password_old" placeholder="Old Password" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {};
};
div class="mb-4" {
label for="password_new" class="block text-sm font-medium text-gray-400" { "New Password" };
input type="password" id="password_new" name="password_new" placeholder="Password" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {};
};
div class="mb-4" {
label for="password_new_repeat" class="block text-sm font-medium text-gray-400" { "Repeat new Password" };
input type="password" id="password_new_repeat" name="password_new_repeat" placeholder="Password" class="w-full mt-1 px-4 py-2 border border-gray-700 bg-gray-700 text-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400" {};
};
input type="hidden" class="csrf" name="csrf" value=(user.get_csrf().await) {};
button type="submit" class="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400" { "Login" };
};
};
};
};
render(content, "Change Password", ctx).await
}