Compare commits
34 Commits
021489c740
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
256d12c9c8
|
|||
|
65edef47b2
|
|||
|
9b69a0a5ee
|
|||
|
e2e9a3efb5
|
|||
|
1f07952973
|
|||
|
b1ccd21068
|
|||
|
7d284f0777
|
|||
|
84dde9cc4b
|
|||
|
e7c0523841
|
|||
|
7fe1b6f8be
|
|||
|
ca726c8e8b
|
|||
|
a08ba568cb
|
|||
|
dd75d89472
|
|||
|
9eb3332576
|
|||
|
032d450af2
|
|||
|
76ac36c4fb
|
|||
|
29804e75e5
|
|||
|
0be4f11f66
|
|||
|
f876ff3f00
|
|||
|
47cd13f734
|
|||
|
cdd296ea84
|
|||
|
4d49a5c0b3
|
|||
| 4aa96dca01 | |||
|
be462dc662
|
|||
| 05d4aca741 | |||
|
ffe1a4d8d2
|
|||
|
24df6054ea
|
|||
|
4229444f96
|
|||
| 202b81e517 | |||
|
1578c3a708
|
|||
| ccc1be0d07 | |||
|
851f73f639
|
|||
| 55c7ad6d6a | |||
|
7a0ef9a3ad
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
.DS_Store
|
||||
/database
|
||||
/mnemodata
|
||||
/scripts
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"languages": {
|
||||
"Rust": {
|
||||
"language_servers": ["rust-analyzer", "tailwindcss-language-server"],
|
||||
"language_servers": [
|
||||
"rust-analyzer",
|
||||
//
|
||||
"tailwindcss-language-server",
|
||||
],
|
||||
},
|
||||
},
|
||||
"lsp": {
|
||||
|
||||
578
Cargo.lock
generated
578
Cargo.lock
generated
@@ -148,6 +148,28 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.40.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.8"
|
||||
@@ -327,6 +349,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
@@ -362,12 +390,31 @@ dependencies = [
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
version = "0.4.37"
|
||||
@@ -403,6 +450,26 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -516,6 +583,12 @@ version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
@@ -525,6 +598,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "1.0.0"
|
||||
@@ -613,6 +695,12 @@ dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
@@ -628,6 +716,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
@@ -716,8 +810,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -727,9 +823,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -746,6 +844,25 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
@@ -882,6 +999,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
@@ -891,6 +1009,22 @@ dependencies = [
|
||||
"pin-utils",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -899,13 +1033,23 @@ version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1053,6 +1197,12 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.10"
|
||||
@@ -1099,6 +1249,55 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-macros",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"simd_cesu8",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-macros"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
||||
dependencies = [
|
||||
"jni-sys-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys-macros"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
@@ -1189,6 +1388,12 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
@@ -1285,10 +1490,12 @@ dependencies = [
|
||||
"chrono-tz",
|
||||
"dotenvy",
|
||||
"env_logger",
|
||||
"http",
|
||||
"log",
|
||||
"maud",
|
||||
"rand 0.10.0",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -1298,6 +1505,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -1359,6 +1567,12 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
@@ -1541,6 +1755,62 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.4",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
@@ -1563,10 +1833,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.0"
|
||||
@@ -1588,6 +1868,16 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
@@ -1597,6 +1887,15 @@ dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.0"
|
||||
@@ -1650,6 +1949,46 @@ version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -1684,12 +2023,28 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -1698,21 +2053,62 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
@@ -1730,12 +2126,53 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[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 = "schannel"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
@@ -1875,6 +2312,22 @@ version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.2"
|
||||
@@ -2183,6 +2636,9 @@ name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
@@ -2195,6 +2651,27 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
@@ -2268,6 +2745,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
@@ -2387,6 +2874,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
@@ -2448,6 +2941,7 @@ dependencies = [
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2486,6 +2980,25 @@ 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 = "want"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
|
||||
dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
@@ -2529,6 +3042,20 @@ dependencies = [
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d24699cd39db9966cf6e2ef10d2f72779c961ad905911f395ea201c3ec9f545d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.109"
|
||||
@@ -2595,6 +3122,35 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "668fa5d00434e890a452ab060d24e3904d1be93f7bb01b70e5603baa2b8ab23b"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
@@ -2623,6 +3179,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.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
@@ -2664,6 +3229,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
|
||||
@@ -13,10 +13,12 @@ chrono = { version = "0.4.43", features = ["serde"] }
|
||||
chrono-tz = "0.10.4"
|
||||
dotenvy = "0.15.7"
|
||||
env_logger = "0.11.9"
|
||||
http = "1.4.0"
|
||||
log = "0.4.29"
|
||||
maud = { version = "0.27.0", features = ["axum"] }
|
||||
rand = "0.10.0"
|
||||
rand08 = { version = "0.8.5", package = "rand" }
|
||||
reqwest = { version = "0.13.3", features = ["json"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
sha2 = "0.10.9"
|
||||
@@ -26,4 +28,5 @@ thiserror = "2.0.18"
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
tower = { version = "0.5.3", features = ["full"] }
|
||||
tower-http = { version = "0.6.8", features = ["full"] }
|
||||
url = { version = "2.5.8", features = ["serde"] }
|
||||
uuid = { version = "1.21.0", features = ["serde", "v7"] }
|
||||
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- pg_volume:/var/lib/postgresql/data:rw
|
||||
- pg_volume:/var/lib/postgresql
|
||||
stop_grace_period: 120s
|
||||
environment:
|
||||
POSTGRES_USER: mnemo
|
||||
|
||||
@@ -28,6 +28,10 @@ pub fn api_router() -> Router<MnemoState> {
|
||||
.route("/api/users/@{handle}", get(users::get_by_handle))
|
||||
.route("/api/users/{id}/setpassw", post(users::change_password))
|
||||
.route("/api/users/{id}/sethandle", post(users::change_handle))
|
||||
.route(
|
||||
"/api/users/{id}/permissions/{perm}",
|
||||
get(users::get_permission).put(users::put_permission),
|
||||
)
|
||||
// sessions
|
||||
.route("/api/sessions/{id}", get(sessions::get_by_id))
|
||||
.route("/api/sessions/{id}/revoke", post(sessions::revoke_by_id))
|
||||
|
||||
@@ -53,6 +53,8 @@ pub struct QuoteCreateForm {
|
||||
pub context: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub public: bool,
|
||||
#[serde(default)]
|
||||
pub discord_webhook: bool,
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
@@ -85,5 +87,12 @@ pub async fn create(
|
||||
|
||||
LogEntry::new(&mut tx, u, LogAction::CreateQuote { id: q.id }).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
if form.discord_webhook {
|
||||
if let Some(ref url) = state.conf.read().await.discord_webhook {
|
||||
q.post_msg_webhook(url.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok((StatusCode::CREATED, Json(q)).into_response())
|
||||
}
|
||||
|
||||
@@ -15,13 +15,14 @@ use crate::{
|
||||
User,
|
||||
auth::{UserAuthRequired, UserAuthenticate},
|
||||
handle::UserHandle,
|
||||
permissions::Permission,
|
||||
permissions::{Permission, PermissionState},
|
||||
},
|
||||
};
|
||||
|
||||
const CANT_CHANGE_OTHERS_HANDLE: &str = "You don't have permission to change this user's handle.";
|
||||
const CANT_CHANGE_OTHERS_PASSW: &str = "You don't have permission to change this user's password.";
|
||||
const CANT_MANUALLY_MAKE_USERS: &str = "You don't have permission to manually create new users.";
|
||||
const GO_AWAY: &str = "You don't have permission to look into permissions!";
|
||||
const HANDLE_CHANGED_SUCCESS: &str = "Handle changed successfully.";
|
||||
const PASSW_CHANGED_SUCCESS: &str = "Password changed successfully.";
|
||||
|
||||
@@ -170,3 +171,82 @@ pub async fn change_password(
|
||||
|
||||
Ok(PASSW_CHANGED_SUCCESS.into_response())
|
||||
}
|
||||
|
||||
pub async fn get_permission(
|
||||
State(state): State<MnemoState>,
|
||||
Path((uid, perm)): Path<(Uuid, Permission)>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let mut conn = state.pool.acquire().await?;
|
||||
let u = User::authenticate(&mut conn, &headers).await?.required()?;
|
||||
if !u.has_permission(&mut conn, Permission::Admin).await? {
|
||||
return Ok((StatusCode::FORBIDDEN, GO_AWAY).into_response());
|
||||
}
|
||||
let target = User::get_by_id(&mut conn, uid).await?;
|
||||
let has: PermissionState = target.permission_dbstate(&mut conn, perm).await?.into();
|
||||
|
||||
Ok((StatusCode::OK, Json(has)).into_response())
|
||||
}
|
||||
|
||||
pub async fn put_permission(
|
||||
State(state): State<MnemoState>,
|
||||
Path((uid, perm)): Path<(Uuid, Permission)>,
|
||||
headers: HeaderMap,
|
||||
Json(newstate): Json<PermissionState>,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let mut tx = state.pool.begin().await?;
|
||||
let u = User::authenticate(&mut tx, &headers).await?.required()?;
|
||||
if !u.has_permission(&mut tx, Permission::Admin).await? {
|
||||
return Ok((StatusCode::FORBIDDEN, GO_AWAY).into_response());
|
||||
}
|
||||
|
||||
let target = User::get_by_id(&mut tx, uid).await?;
|
||||
let os: PermissionState = target.permission_dbstate(&mut tx, perm).await?.into();
|
||||
match newstate {
|
||||
PermissionState::ExplicitlyGranted => {
|
||||
target.grant_permission(&mut tx, perm).await?;
|
||||
LogEntry::new(
|
||||
&mut tx,
|
||||
u,
|
||||
LogAction::UpdatePermission {
|
||||
id: target.id,
|
||||
os,
|
||||
ns: newstate,
|
||||
p: perm,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
PermissionState::ExplicitlyRevoked => {
|
||||
target.revoke_permission(&mut tx, perm).await?;
|
||||
LogEntry::new(
|
||||
&mut tx,
|
||||
u,
|
||||
LogAction::UpdatePermission {
|
||||
id: target.id,
|
||||
os,
|
||||
ns: newstate,
|
||||
p: perm,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
PermissionState::Implicit => {
|
||||
target.reset_permission(&mut tx, perm).await?;
|
||||
LogEntry::new(
|
||||
&mut tx,
|
||||
u,
|
||||
LogAction::UpdatePermission {
|
||||
id: target.id,
|
||||
os,
|
||||
ns: newstate,
|
||||
p: perm,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
};
|
||||
|
||||
tx.commit().await?;
|
||||
Ok((StatusCode::OK, Json(newstate)).into_response())
|
||||
}
|
||||
|
||||
@@ -8,10 +8,55 @@ use std::{
|
||||
use env_logger::fmt::Formatter;
|
||||
use log::{LevelFilter, Record};
|
||||
use sqlx::PgPool;
|
||||
use url::Url;
|
||||
|
||||
/// Mnemosyne, the mother of the nine muses
|
||||
pub const DEFAULT_PORT: u16 = 0x9999; // 39321
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct MnemoConf {
|
||||
pub instance_name: String,
|
||||
pub discord_webhook: Option<Url>,
|
||||
}
|
||||
|
||||
impl MnemoConf {
|
||||
pub async fn load(conn: &mut sqlx::PgConnection) -> Result<Self, sqlx::Error> {
|
||||
let row: Option<serde_json::Value> =
|
||||
sqlx::query_scalar("SELECT config FROM mnemoconf LIMIT 1")
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?;
|
||||
|
||||
Ok(match row {
|
||||
Some(val) => serde_json::from_value(val).unwrap_or_default(),
|
||||
None => {
|
||||
let conf = MnemoConf::default();
|
||||
conf.save(conn).await?;
|
||||
conf
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn save(&self, conn: &mut sqlx::PgConnection) -> Result<(), sqlx::Error> {
|
||||
let val = serde_json::to_value(self).unwrap();
|
||||
sqlx::query("DELETE FROM mnemoconf")
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
sqlx::query("INSERT INTO mnemoconf (config) VALUES ($1)")
|
||||
.bind(val)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Default for MnemoConf {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
instance_name: String::from("Mnemosyne"),
|
||||
discord_webhook: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const REFERENCE_SPLASHES: &[&str] = &[
|
||||
"quote engine",
|
||||
"powered by rust",
|
||||
|
||||
3
src/database/migrations/0002_conf.sql
Normal file
3
src/database/migrations/0002_conf.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
CREATE TABLE mnemoconf (
|
||||
config JSONB NOT NULL
|
||||
);
|
||||
51
src/logs.rs
51
src/logs.rs
@@ -1,9 +1,18 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{PgConnection, Row};
|
||||
use strum::{EnumDiscriminants, EnumIter, IntoStaticStr, VariantNames};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{database::DatabaseError, users::User, web::icons};
|
||||
use crate::{
|
||||
database::DatabaseError,
|
||||
quotes::Quote,
|
||||
users::{
|
||||
User,
|
||||
permissions::{Permission, PermissionState},
|
||||
},
|
||||
web::icons,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LogEntry {
|
||||
@@ -105,6 +114,12 @@ pub enum LogAction {
|
||||
id: Uuid,
|
||||
handle: String,
|
||||
},
|
||||
UpdatePermission {
|
||||
id: Uuid,
|
||||
os: PermissionState,
|
||||
ns: PermissionState,
|
||||
p: Permission,
|
||||
},
|
||||
ManuallyChangeUsersPassword {
|
||||
id: Uuid,
|
||||
},
|
||||
@@ -151,14 +166,26 @@ pub enum LogAction {
|
||||
CreateQuote {
|
||||
id: Uuid,
|
||||
},
|
||||
DeleteQuote {
|
||||
quote: Quote,
|
||||
},
|
||||
ManuallyRevokeSession {
|
||||
id: Uuid,
|
||||
},
|
||||
ChangeInstanceName {
|
||||
old: String,
|
||||
new: String,
|
||||
},
|
||||
ChangeDiscordWebhookUrl {
|
||||
old: Option<Url>,
|
||||
new: Option<Url>,
|
||||
},
|
||||
}
|
||||
impl LogAction {
|
||||
pub fn get_target_id(&self) -> Option<Uuid> {
|
||||
match self {
|
||||
Self::Initialize | Self::RegenInfradmin => None,
|
||||
|
||||
Self::CreateUser { id, .. }
|
||||
| Self::CreateTag { id, .. }
|
||||
| Self::CreatePerson { id, .. }
|
||||
@@ -167,10 +194,18 @@ impl LogAction {
|
||||
| Self::ManuallyRevokeSession { id }
|
||||
| Self::RenameTag { id, .. }
|
||||
| Self::DeleteTag { id, .. }
|
||||
| Self::UpdatePermission { id, .. }
|
||||
| Self::ManuallyChangeUsersPassword { id } => Some(*id),
|
||||
|
||||
Self::DeleteQuote { quote } => Some(quote.id),
|
||||
|
||||
Self::AddPersonName { pid, .. }
|
||||
| Self::DeletePersonName { pid, .. }
|
||||
| Self::SetPersonPrimaryName { pid, .. } => Some(*pid),
|
||||
|
||||
Self::ChangeInstanceName { .. } | Self::ChangeDiscordWebhookUrl { .. } => {
|
||||
Some(Uuid::nil())
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn get_humanreadable_payload(&self) -> String {
|
||||
@@ -180,6 +215,9 @@ impl LogAction {
|
||||
LogAction::CreateUser { id, handle } => {
|
||||
format!("Created user @{handle} (uid: {id})")
|
||||
}
|
||||
LogAction::UpdatePermission { id, os, ns, p } => {
|
||||
format!("Updated permission {p} of user with id {id} from {os} to {ns}")
|
||||
}
|
||||
LogAction::ManuallyChangeUsersPassword { id } => {
|
||||
format!("Manually changed password of user with id: {id}")
|
||||
}
|
||||
@@ -210,9 +248,16 @@ impl LogAction {
|
||||
LogAction::CreateQuote { id } => {
|
||||
format!("Created quote of ID {id}")
|
||||
}
|
||||
LogAction::DeleteQuote { quote } => {
|
||||
format!("Deleted quote of ID {}", quote.id)
|
||||
}
|
||||
LogAction::ManuallyRevokeSession { id } => {
|
||||
format!("Revoked session of ID {id}")
|
||||
}
|
||||
LogAction::ChangeInstanceName { old, new } => {
|
||||
format!("Changed instance name from \"{old}\" to \"{new}\"")
|
||||
}
|
||||
LogAction::ChangeDiscordWebhookUrl { .. } => "Changed Discord webhook URL".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,6 +269,7 @@ impl LogActionDiscriminant {
|
||||
LAD::Initialize => "Mnemosyne Initialization",
|
||||
LAD::RegenInfradmin => "Infradmin Regeneration",
|
||||
LAD::CreateUser => "User Creation",
|
||||
LAD::UpdatePermission => "Permission Update",
|
||||
LAD::ManuallyChangeUsersPassword => "Password Override",
|
||||
LAD::CreateTag => "Tag Creation",
|
||||
LAD::RenameTag => "Tag Rename",
|
||||
@@ -234,7 +280,10 @@ impl LogActionDiscriminant {
|
||||
LAD::DeletePersonName => "Person Name Deletion",
|
||||
LAD::SetPersonPrimaryName => "Person Primary Name Set",
|
||||
LAD::CreateQuote => "Quote Creation",
|
||||
LAD::DeleteQuote => "Quote Deletion",
|
||||
LAD::ManuallyRevokeSession => "Manual Session Revocation",
|
||||
LAD::ChangeInstanceName => "Instance Name Change",
|
||||
LAD::ChangeDiscordWebhookUrl => "Discord Webhook URL Change",
|
||||
}
|
||||
}
|
||||
pub fn icon(&self) -> &'static str {
|
||||
|
||||
12
src/main.rs
12
src/main.rs
@@ -1,8 +1,10 @@
|
||||
use std::error::Error;
|
||||
use std::{error::Error, sync::Arc};
|
||||
|
||||
use axum::Router;
|
||||
use sqlx::PgPool;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::{net::TcpListener, sync::RwLock};
|
||||
|
||||
use crate::config::MnemoConf;
|
||||
|
||||
mod api;
|
||||
mod config;
|
||||
@@ -21,6 +23,7 @@ const ISE_MSG: &str = "Internal server error";
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MnemoState {
|
||||
pool: PgPool,
|
||||
conf: Arc<RwLock<MnemoConf>>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -31,6 +34,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let pool = config::init_pool().await?;
|
||||
sqlx::migrate!("src/database/migrations").run(&pool).await?;
|
||||
log::info!("Migrations applied successfully.");
|
||||
let conf = Arc::new(RwLock::new(
|
||||
MnemoConf::load(&mut *pool.acquire().await?).await?,
|
||||
));
|
||||
users::auth::init_password_dummies();
|
||||
users::setup::initialise_reserved_users_if_needed(&pool).await?;
|
||||
|
||||
@@ -38,7 +44,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let r = Router::new()
|
||||
.merge(api::api_router())
|
||||
.merge(web::web_router())
|
||||
.with_state(MnemoState { pool });
|
||||
.with_state(MnemoState { pool, conf });
|
||||
let l = TcpListener::bind(format!("0.0.0.0:{port}")).await?;
|
||||
log::info!("Listener bound to {}", l.local_addr()?);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{PgConnection, Row};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct Person {
|
||||
pub primary_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Name {
|
||||
pub id: Uuid,
|
||||
pub is_primary: bool,
|
||||
|
||||
@@ -3,13 +3,15 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{PgConnection, Row};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{database::DatabaseError, persons::Name};
|
||||
|
||||
#[derive(Serialize)]
|
||||
mod webhook;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Quote {
|
||||
pub id: Uuid,
|
||||
pub lines: Vec<QuoteLine>,
|
||||
@@ -20,7 +22,7 @@ pub struct Quote {
|
||||
pub public: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct QuoteLine {
|
||||
pub id: Uuid,
|
||||
pub attribution: Vec<Name>,
|
||||
@@ -288,6 +290,40 @@ impl Quote {
|
||||
public,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn delete(self, conn: &mut PgConnection) -> Result<(), QuoteError> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM line_authors
|
||||
WHERE line_id IN (SELECT id FROM lines WHERE quote_id = $1)
|
||||
"#,
|
||||
)
|
||||
.bind(self.id)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
sqlx::query("DELETE FROM lines WHERE quote_id = $1")
|
||||
.bind(self.id)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
sqlx::query("DELETE FROM quote_tags WHERE quote_id = $1")
|
||||
.bind(self.id)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
sqlx::query("DELETE FROM user_quote_likes WHERE quote_id = $1")
|
||||
.bind(self.id)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
sqlx::query("DELETE FROM quotes WHERE id = $1")
|
||||
.bind(self.id)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for QuoteError {
|
||||
|
||||
56
src/quotes/webhook.rs
Normal file
56
src/quotes/webhook.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use reqwest::Url;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::quotes::Quote;
|
||||
|
||||
impl Quote {
|
||||
pub fn post_msg_webhook(&self, url: Url) {
|
||||
let mut message = String::new();
|
||||
for line in &self.lines {
|
||||
let authors = line
|
||||
.attribution
|
||||
.iter()
|
||||
.map(|n| n.name.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
message.push_str(&format!("> {}\n", line.content));
|
||||
message.push_str(&format!("~ {}\n", authors));
|
||||
}
|
||||
|
||||
message.push_str(&format!("\n-# {}", self.timestamp));
|
||||
let escape_md = |s: &str| s.replace('*', "\\*").replace('_', "\\_");
|
||||
|
||||
if let Some(ctx) = &self.location {
|
||||
message.push_str(&format!(" | Location: {}", escape_md(ctx)));
|
||||
}
|
||||
if let Some(ctx) = &self.context {
|
||||
message.push_str(&format!(" | Context: *{}*", escape_md(ctx)));
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
let client = match reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::error!("Failed to construct reqwest Client while sending webhook! {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let body = serde_json::json!({
|
||||
"content": message
|
||||
});
|
||||
match client.post(url).json(&body).send().await {
|
||||
Ok(response) => {
|
||||
if let Err(e) = response.error_for_status() {
|
||||
log::error!("Webhook responded with an HTTP error! {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to POST webhook! {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ impl User {
|
||||
}
|
||||
|
||||
pub async fn get_all(conn: &mut PgConnection) -> Result<Vec<User>, UserError> {
|
||||
let rows = sqlx::query("SELECT id, handle FROM users")
|
||||
let rows = sqlx::query("SELECT id, handle FROM users ORDER BY id")
|
||||
.fetch_all(conn)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
use sqlx::PgConnection;
|
||||
use strum::Display;
|
||||
|
||||
use crate::{database::DatabaseError, users::User};
|
||||
|
||||
/// Infradmin and systemuser have all permissions.
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
strum::IntoStaticStr,
|
||||
Display,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
)]
|
||||
pub enum Permission {
|
||||
// Pass all the permission checks
|
||||
// Additionally, only Admins can manage others' permissions.
|
||||
Admin,
|
||||
// All Users have the right to observe their own sessions
|
||||
ListOthersSessions,
|
||||
// All Users have the right to revoke their own sessions
|
||||
@@ -16,23 +30,130 @@ pub enum Permission {
|
||||
CreateTags,
|
||||
RenameTags,
|
||||
DeleteTags,
|
||||
CreateQuotes,
|
||||
DeleteQuotes,
|
||||
ChangePersonPrimaryName,
|
||||
#[allow(unused)]
|
||||
BrowseServerLogs,
|
||||
ConfigureInstance,
|
||||
}
|
||||
|
||||
impl Permission {
|
||||
pub fn is_default_permission(&self) -> bool {
|
||||
match self {
|
||||
Self::CreateTags | Self::CreateQuotes => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
strum::IntoStaticStr,
|
||||
Display,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
)]
|
||||
pub enum PermissionState {
|
||||
ExplicitlyGranted,
|
||||
ExplicitlyRevoked,
|
||||
Implicit,
|
||||
}
|
||||
|
||||
impl From<Option<bool>> for PermissionState {
|
||||
fn from(state: Option<bool>) -> Self {
|
||||
match state {
|
||||
Some(true) => Self::ExplicitlyGranted,
|
||||
Some(false) => Self::ExplicitlyRevoked,
|
||||
None => Self::Implicit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn permission_dbstate(
|
||||
&self,
|
||||
conn: &mut PgConnection,
|
||||
permission: Permission,
|
||||
) -> Result<Option<bool>, DatabaseError> {
|
||||
let permission_key: &'static str = (&permission).into();
|
||||
let state: Option<bool> = sqlx::query_scalar(
|
||||
"SELECT state FROM user_permissions WHERE user_id = $1 AND permission = $2",
|
||||
)
|
||||
.bind(self.id)
|
||||
.bind(permission_key)
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?;
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub async fn has_permission(
|
||||
&self,
|
||||
#[allow(unused)] conn: &mut PgConnection,
|
||||
#[allow(unused)] permission: Permission,
|
||||
conn: &mut PgConnection,
|
||||
permission: Permission,
|
||||
) -> Result<bool, DatabaseError> {
|
||||
// Infradmin and systemuser have all permissions
|
||||
if self.is_infradmin() || self.is_systemuser() {
|
||||
return Ok(true);
|
||||
}
|
||||
if let Some(true) = self.permission_dbstate(conn, Permission::Admin).await? {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
// todo!("Do the permission checking here once permissions are modeled in the DB")
|
||||
Ok(self
|
||||
.permission_dbstate(conn, permission)
|
||||
.await?
|
||||
.unwrap_or(permission.is_default_permission()))
|
||||
}
|
||||
|
||||
pub async fn grant_permission(
|
||||
&self,
|
||||
conn: &mut PgConnection,
|
||||
permission: Permission,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let permission_key: &'static str = (&permission).into();
|
||||
sqlx::query(
|
||||
"INSERT INTO user_permissions (user_id, permission, state) VALUES ($1, $2, TRUE) ON CONFLICT (user_id, permission) DO UPDATE SET state = EXCLUDED.state",
|
||||
)
|
||||
.bind(self.id)
|
||||
.bind(permission_key)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn revoke_permission(
|
||||
&self,
|
||||
conn: &mut PgConnection,
|
||||
permission: Permission,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let permission_key: &'static str = (&permission).into();
|
||||
sqlx::query(
|
||||
"INSERT INTO user_permissions (user_id, permission, state) VALUES ($1, $2, FALSE) ON CONFLICT (user_id, permission) DO UPDATE SET state = EXCLUDED.state",
|
||||
)
|
||||
.bind(self.id)
|
||||
.bind(permission_key)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reset_permission(
|
||||
&self,
|
||||
conn: &mut PgConnection,
|
||||
permission: Permission,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let permission_key: &'static str = (&permission).into();
|
||||
sqlx::query("DELETE FROM user_permissions WHERE user_id = $1 AND permission = $2")
|
||||
.bind(self.id)
|
||||
.bind(permission_key)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
use sqlx::PgConnection;
|
||||
|
||||
use crate::{users::User, web::icons};
|
||||
use crate::{
|
||||
users::{User, permissions::Permission},
|
||||
web::icons,
|
||||
};
|
||||
|
||||
// (SHOWTEXT, LINK, ICON, REQUIRES_LOG_IN)
|
||||
const LINKS: &[(&str, &str, &str, bool)] = &[
|
||||
@@ -13,7 +17,12 @@ const LINKS: &[(&str, &str, &str, bool)] = &[
|
||||
("Logs", "/logs", icons::CLIPBOARD_CLOCK, true),
|
||||
];
|
||||
|
||||
pub fn nav(user: Option<&User>, uri: &str) -> Markup {
|
||||
pub async fn nav(conn: &mut PgConnection, user: Option<&User>, uri: &str) -> Markup {
|
||||
#[rustfmt::skip]
|
||||
let show_instance_conf = match user {
|
||||
Some(u) if u.has_permission(conn, Permission::ConfigureInstance).await.is_ok_and(|r| r) => true,
|
||||
_ => false,
|
||||
};
|
||||
html!(
|
||||
div class="flex items-center text-sm gap-4 border-b border-neutral-200/25 bg-neutral-200/5 px-4 py-2" {
|
||||
a href="/dashboard" class="font-lora font-semibold hidden xs:block md:text-xl sm:mr-2" {"Mnemosyne"}
|
||||
@@ -46,15 +55,22 @@ pub fn nav(user: Option<&User>, uri: &str) -> Markup {
|
||||
span class="hidden sm:block"{(u.handle)}
|
||||
div class="scale-[.75]" {(PreEscaped(icons::USER))}
|
||||
}
|
||||
div class="absolute right-0 top-full pt-1 w-40 opacity-0 invisible group-focus-within:opacity-100 group-focus-within:visible transition-all duration-100 z-50" {
|
||||
div class="absolute right-0 top-full pt-1 w-44 opacity-0 invisible group-focus-within:opacity-100 group-focus-within:visible transition-all duration-100 z-50" {
|
||||
div class="rounded bg-neutral-900 border border-neutral-200/25 shadow-lg flex flex-col overflow-hidden" {
|
||||
a href=(format!("/users/{}", u.id)) class="px-4 py-2 flex items-center gap-2 hover:bg-neutral-200/10 font-lexend text-sm text-neutral-200 transition-colors" {
|
||||
div class="scale-[.7]" {(PreEscaped(icons::USER))}
|
||||
p {"Profile"}
|
||||
}
|
||||
a href="/user-settings" class="px-4 py-2 flex items-center gap-2 hover:bg-neutral-200/10 font-lexend text-sm text-neutral-200 transition-colors" {
|
||||
div class="scale-[.7]" {(PreEscaped(icons::SETTINGS))}
|
||||
p {"User Settings"}
|
||||
}
|
||||
@if show_instance_conf {
|
||||
div class="h-px w-full bg-neutral-200/15" {}
|
||||
a href="/instance-config" class="px-4 py-2 flex items-center gap-2 hover:bg-neutral-200/10 font-lexend text-sm text-neutral-200 transition-colors" {
|
||||
div class="scale-[.7]" {(PreEscaped(icons::SERVER))}
|
||||
p {"Settings"}
|
||||
p {"Instance Config"}
|
||||
}
|
||||
}
|
||||
div class="h-px w-full bg-neutral-200/15" {}
|
||||
form action="/api/auth/logout-form" method="post" {
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{quotes::Quote, web::icons};
|
||||
|
||||
pub fn quote(quote: &Quote) -> Markup {
|
||||
html!(
|
||||
div class="border border-neutral-200/25 bg-neutral-200/5 p-3 pb-1 overflow-clip rounded-md relative flex flex-col" {
|
||||
div class="border border-neutral-200/25 bg-neutral-200/5 p-3 pb-1 overflow-clip rounded-md relative flex flex-col transition-colors group-hover/a:border-neutral-200/35 group-hover/a:bg-neutral-200/10" {
|
||||
div class="absolute top-4 right-6 -rotate-12 opacity-[.025] scale-x-[4.5] scale-y-[4]" {
|
||||
(PreEscaped(icons::QUOTE))
|
||||
}
|
||||
|
||||
1
src/web/icons/code.svg
Normal file
1
src/web/icons/code.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code-icon lucide-code"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
|
||||
|
After Width: | Height: | Size: 282 B |
1
src/web/icons/message-square-code.svg
Normal file
1
src/web/icons/message-square-code.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-square-code-icon lucide-message-square-code"><path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"/><path d="m10 8-3 3 3 3"/><path d="m14 14 3-3-3-3"/></svg>
|
||||
|
After Width: | Height: | Size: 440 B |
@@ -1,3 +1,4 @@
|
||||
#![allow(unused)]
|
||||
// Below icons sourced from https://lucide.dev
|
||||
pub const ARROW_RIGHT: &str = include_str!("arrow-right.svg");
|
||||
pub const CALENDAR_1: &str = include_str!("calendar-1.svg");
|
||||
@@ -5,6 +6,7 @@ pub const CALENDAR_ARROW_DOWN: &str = include_str!("calendar-arrow-down.svg");
|
||||
pub const CIRCLE_MINUS: &str = include_str!("circle-minus.svg");
|
||||
pub const CLIPBOARD_CLOCK: &str = include_str!("clipboard-clock.svg");
|
||||
pub const CLOCK: &str = include_str!("clock.svg");
|
||||
pub const CODE: &str = include_str!("code.svg");
|
||||
pub const CONTACT: &str = include_str!("contact.svg");
|
||||
pub const EYE: &str = include_str!("eye.svg");
|
||||
pub const FILE_IMAGE: &str = include_str!("file-image.svg");
|
||||
@@ -14,13 +16,18 @@ pub const LAYOUT_DASHBOARD: &str = include_str!("layout-dashboard.svg");
|
||||
pub const LINE_DOT_RIGHT_HORIZONTAL: &str = include_str!("line-dot-right-horizontal.svg");
|
||||
pub const LOG_OUT: &str = include_str!("log-out.svg");
|
||||
pub const MAP_PIN: &str = include_str!("map-pin.svg");
|
||||
pub const MESSAGE_SQUARE_CODE: &str = include_str!("message-square-code.svg");
|
||||
pub const PEN: &str = include_str!("pen.svg");
|
||||
pub const PLUS: &str = include_str!("plus.svg");
|
||||
pub const QUOTE: &str = include_str!("quote.svg");
|
||||
pub const REFRESH_CW: &str = include_str!("refresh-cw.svg");
|
||||
pub const SCROLL_TEXT: &str = include_str!("scroll-text.svg");
|
||||
pub const SERVER: &str = include_str!("server.svg");
|
||||
pub const SETTINGS: &str = include_str!("settings.svg");
|
||||
pub const SHIELD_USER: &str = include_str!("shield-user.svg");
|
||||
pub const TAG: &str = include_str!("tag.svg");
|
||||
pub const TRASH: &str = include_str!("trash.svg");
|
||||
pub const TYPE: &str = include_str!("type.svg");
|
||||
pub const USER: &str = include_str!("user.svg");
|
||||
pub const USER_KEY: &str = include_str!("user-key.svg");
|
||||
pub const USER_PLUS: &str = include_str!("user-plus.svg");
|
||||
|
||||
1
src/web/icons/refresh-cw.svg
Normal file
1
src/web/icons/refresh-cw.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-refresh-cw-icon lucide-refresh-cw"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
|
||||
|
After Width: | Height: | Size: 412 B |
1
src/web/icons/settings.svg
Normal file
1
src/web/icons/settings.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-settings-icon lucide-settings"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
|
After Width: | Height: | Size: 610 B |
1
src/web/icons/trash.svg
Normal file
1
src/web/icons/trash.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-icon lucide-trash"><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
|
After Width: | Height: | Size: 355 B |
1
src/web/icons/type.svg
Normal file
1
src/web/icons/type.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-type-icon lucide-type"><path d="M12 4v16"/><path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2"/><path d="M9 20h6"/></svg>
|
||||
|
After Width: | Height: | Size: 322 B |
232
src/web/pages/conf.rs
Normal file
232
src/web/pages/conf.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use axum::{
|
||||
Form,
|
||||
extract::{Request, State},
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use http::StatusCode;
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
MnemoState,
|
||||
error::CompositeError,
|
||||
logs::{LogAction, LogEntry},
|
||||
users::{
|
||||
User,
|
||||
auth::{UserAuthRequired, UserAuthenticate},
|
||||
permissions::Permission,
|
||||
},
|
||||
web::{components::nav::nav, icons, pages::base},
|
||||
};
|
||||
|
||||
pub async fn page(
|
||||
State(state): State<MnemoState>,
|
||||
req: Request,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let mut conn = state.pool.acquire().await?;
|
||||
let u = match User::authenticate(&mut *conn, req.headers()).await? {
|
||||
Some(u) => u,
|
||||
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
|
||||
};
|
||||
|
||||
if !u
|
||||
.has_permission(&mut *conn, Permission::ConfigureInstance)
|
||||
.await?
|
||||
{
|
||||
return Ok((
|
||||
StatusCode::FORBIDDEN,
|
||||
"You do not have permission to view this page.",
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let (current_name, current_webhook) = {
|
||||
let conf = state.conf.read().await;
|
||||
let current_name = conf.instance_name.clone();
|
||||
let current_webhook = conf
|
||||
.discord_webhook
|
||||
.as_ref()
|
||||
.map(|u| u.to_string())
|
||||
.unwrap_or_default();
|
||||
(current_name, current_webhook)
|
||||
};
|
||||
|
||||
Ok(base(
|
||||
"Instance Config | Mnemosyne",
|
||||
html!(
|
||||
(nav(&mut conn, Some(&u), req.uri().path()).await)
|
||||
|
||||
div class="max-w-4xl mx-auto px-2" {
|
||||
div class="mx-auto max-w-4xl my-4 mb-8" {
|
||||
p class="flex items-center gap-2" {
|
||||
span class="text-neutral-500" {(PreEscaped(icons::SERVER))}
|
||||
span class="text-2xl font-semibold font-lora" {"Mnemosyne Instance Settings"}
|
||||
}
|
||||
p class="text-neutral-500 text-sm font-light mt-1" {
|
||||
"Manage global configuration for your Mnemosyne instance."
|
||||
}
|
||||
}
|
||||
|
||||
(setting_block(
|
||||
"Instance Name",
|
||||
"The name of this instance. This is displayed on the dashboard, in page titles, and used in webhook payloads to identify this server.",
|
||||
icons::PEN,
|
||||
"/instance-config/name",
|
||||
"instance_name",
|
||||
"text",
|
||||
"e.g. Mnemosyne",
|
||||
¤t_name,
|
||||
))
|
||||
|
||||
hr class="mt-6 mb-4 border-neutral-600";
|
||||
|
||||
(setting_block(
|
||||
"Discord Webhook URL",
|
||||
"Mnemosyne will attempt to send a message to this webhook for all newly created quotes, regardless of whether they are public or not. Leave empty to disable.",
|
||||
icons::MESSAGE_SQUARE_CODE,
|
||||
"/instance-config/dsc-webhook",
|
||||
"webhook_url",
|
||||
"url",
|
||||
"https://discord.com/api/webhooks/...",
|
||||
¤t_webhook,
|
||||
))
|
||||
}
|
||||
),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
fn setting_block(
|
||||
title: &str,
|
||||
description: &str,
|
||||
icon: &str,
|
||||
form_action: &str,
|
||||
input_name: &str,
|
||||
input_type: &str,
|
||||
input_placeholder: &str,
|
||||
current_value: &str,
|
||||
) -> Markup {
|
||||
html! {
|
||||
div class="mb-6" {
|
||||
p class="flex items-center gap-1" {
|
||||
span class="text-neutral-500 scale-[.8]" {(PreEscaped(icon))}
|
||||
span class="text-lg font-semibold font-lora" {(title)}
|
||||
}
|
||||
p class="text-neutral-500 text-sm font-light mb-3" {
|
||||
(description)
|
||||
}
|
||||
form action=(form_action) method="post" class="flex gap-2" {
|
||||
input id=(input_name) name=(input_name) type=(input_type) placeholder=(input_placeholder) autocomplete="off" value=(current_value)
|
||||
class="w-full max-w-md px-2 py-1 border border-neutral-200/25 bg-neutral-950/50 rounded outline-none focus:border-neutral-200/50";
|
||||
button type="submit" class="px-4 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:border-neutral-200/40" {
|
||||
"Save"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct InstanceNameForm {
|
||||
instance_name: String,
|
||||
}
|
||||
pub async fn change_name(
|
||||
State(state): State<MnemoState>,
|
||||
headers: HeaderMap,
|
||||
Form(form): Form<InstanceNameForm>,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let mut tx = state.pool.begin().await?;
|
||||
let u = User::authenticate(&mut tx, &headers).await?.required()?;
|
||||
if !u
|
||||
.has_permission(&mut tx, Permission::ConfigureInstance)
|
||||
.await?
|
||||
{
|
||||
return Ok((
|
||||
StatusCode::FORBIDDEN,
|
||||
"You do not have permission to change this.",
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
if form.instance_name.trim().is_empty() {
|
||||
return Ok((StatusCode::BAD_REQUEST, "Instance name cannot be empty.").into_response());
|
||||
}
|
||||
|
||||
let new_name = form.instance_name.trim().to_string();
|
||||
|
||||
LogEntry::new(
|
||||
&mut tx,
|
||||
u,
|
||||
LogAction::ChangeInstanceName {
|
||||
old: state.conf.read().await.instance_name.clone(),
|
||||
new: new_name.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut new_conf = state.conf.read().await.clone();
|
||||
new_conf.instance_name = new_name.clone();
|
||||
new_conf.save(&mut tx).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
state.conf.write().await.instance_name = new_name;
|
||||
Ok(Redirect::to("/instance-config").into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct WebhookForm {
|
||||
webhook_url: String,
|
||||
}
|
||||
pub async fn change_webhook(
|
||||
State(state): State<MnemoState>,
|
||||
headers: HeaderMap,
|
||||
Form(form): Form<WebhookForm>,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let mut tx = state.pool.begin().await?;
|
||||
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
|
||||
if !u
|
||||
.has_permission(&mut tx, Permission::ConfigureInstance)
|
||||
.await?
|
||||
{
|
||||
return Ok((
|
||||
StatusCode::FORBIDDEN,
|
||||
"You do not have permission to change this.",
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let new_webhook = if form.webhook_url.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
match Url::parse(form.webhook_url.trim()) {
|
||||
Ok(url) => Some(url),
|
||||
Err(_) => {
|
||||
return Ok(
|
||||
(axum::http::StatusCode::BAD_REQUEST, "Invalid URL format.").into_response()
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LogEntry::new(
|
||||
&mut tx,
|
||||
u,
|
||||
LogAction::ChangeDiscordWebhookUrl {
|
||||
old: state.conf.read().await.discord_webhook.clone(),
|
||||
new: new_webhook.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut new_conf = state.conf.read().await.clone();
|
||||
new_conf.discord_webhook = new_webhook.clone();
|
||||
new_conf.save(&mut tx).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
state.conf.write().await.discord_webhook = new_webhook;
|
||||
Ok(Redirect::to("/instance-config").into_response())
|
||||
}
|
||||
@@ -51,7 +51,7 @@ pub async fn page(
|
||||
Ok(base(
|
||||
"Dashboard | Mnemosyne",
|
||||
html!(
|
||||
(nav(u.as_ref(), req.uri().path()))
|
||||
(nav(&mut conn, u.as_ref(), req.uri().path()).await)
|
||||
|
||||
div class="mx-auto max-w-4xl px-2 mt-4 grid grid-cols-1 --sm:grid-cols-2 gap-4" {
|
||||
div class="flex flex-col" {
|
||||
@@ -61,19 +61,26 @@ pub async fn page(
|
||||
"This just in! This quote was added "
|
||||
(format_time_ago(q.get_creation_timestamp())) " ago."
|
||||
}
|
||||
div class="flex-1 [&>div]:h-full" {(quote(q))}
|
||||
div class="flex-1 [&>div]:h-full" {
|
||||
a href=(format!("/quotes/{}", q.id)) class="group/a" {(quote(q))}
|
||||
}
|
||||
} @else {
|
||||
p class="text-neutral-500 font-light mb-4" {"No quotes yet."}
|
||||
}
|
||||
}
|
||||
@if let Some(q) = random_quote {
|
||||
div class="flex flex-col" {
|
||||
div class="flex gap-1" {
|
||||
p {"Random Quote"}
|
||||
a href="/dashboard" class="text-neutral-500 scale-[.65] hover:text-neutral-200 focus:text-neutral-200" {(PreEscaped(icons::REFRESH_CW))}
|
||||
}
|
||||
p class="text-neutral-500 font-light mb-4" {
|
||||
"This quote was added "
|
||||
(format_time_ago(q.get_creation_timestamp())) " ago."
|
||||
}
|
||||
div class="flex-1 [&>div]:h-full" {(quote(&q))}
|
||||
div class="flex-1 [&>div]:h-full" {
|
||||
a href=(format!("/quotes/{}", q.id)) class="group/a" {(quote(&q))}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ pub async fn page(
|
||||
Ok(base(
|
||||
"Logs | Mnemosyne",
|
||||
html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
(nav(&mut tx, Some(&u), req.uri().path()).await)
|
||||
|
||||
@if true {//let Ok(true) = u.has_permission(&mut *tx, Permission::BrowseServerLogs) {
|
||||
div class="max-w-4xl mx-auto px-2" {
|
||||
|
||||
@@ -6,6 +6,7 @@ use maud::{DOCTYPE, Markup, html};
|
||||
|
||||
use crate::MnemoState;
|
||||
|
||||
pub mod conf;
|
||||
pub mod dashboard;
|
||||
pub mod index;
|
||||
pub mod login;
|
||||
@@ -22,9 +23,15 @@ pub fn pages() -> Router<MnemoState> {
|
||||
.route("/", get(index::page))
|
||||
.route("/login", get(login::page))
|
||||
.route("/dashboard", get(dashboard::page))
|
||||
//
|
||||
.route("/instance-config", get(conf::page))
|
||||
.route("/instance-config/name", post(conf::change_name))
|
||||
.route("/instance-config/dsc-webhook", post(conf::change_webhook))
|
||||
//
|
||||
.route("/user-settings", get(usersettings::page))
|
||||
.route("/user-settings/handle", post(usersettings::change_handle))
|
||||
.route("/user-settings/passwd", post(usersettings::change_password))
|
||||
//
|
||||
.route("/users", get(users::page))
|
||||
.route("/users/{id}", get(users::profile::page))
|
||||
.route("/users/create", get(users::create::page))
|
||||
@@ -43,6 +50,11 @@ pub fn pages() -> Router<MnemoState> {
|
||||
.route("/logs", get(logs::page))
|
||||
//
|
||||
.route("/quotes", get(quotes::page))
|
||||
.route("/quotes/{id}", get(quotes::id::page))
|
||||
.route(
|
||||
"/quotes/{id}/delete",
|
||||
get(quotes::id::delete_confirm).post(quotes::id::delete),
|
||||
)
|
||||
.route("/quotes/add", get(quotes::add::page))
|
||||
.route("/quotes/add-form", post(quotes::add::form))
|
||||
//
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use axum::extract::{Request, State};
|
||||
use maud::{Markup, html};
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use http::StatusCode;
|
||||
use maud::html;
|
||||
|
||||
use crate::{
|
||||
MnemoState,
|
||||
@@ -8,16 +12,19 @@ use crate::{
|
||||
web::{components::nav::nav, pages::base},
|
||||
};
|
||||
|
||||
pub async fn page(State(state): State<MnemoState>, req: Request) -> Result<Markup, CompositeError> {
|
||||
pub async fn page(
|
||||
State(state): State<MnemoState>,
|
||||
req: Request,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let mut conn = state.pool.acquire().await?;
|
||||
let u = User::authenticate(&mut *conn, req.headers())
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
Ok(base(
|
||||
Ok((StatusCode::NOT_FOUND, base(
|
||||
"Not Found | Mnemosyne",
|
||||
html!(
|
||||
(nav(u.as_ref(), req.uri().path()))
|
||||
(nav(&mut conn, u.as_ref(), req.uri().path()).await)
|
||||
|
||||
div class="mx-auto max-w-4xl px-2 mt-8 mb-2" {
|
||||
h1 class="text-4xl font-lora font-semibold mb-1" { "Not Found" }
|
||||
@@ -27,5 +34,5 @@ pub async fn page(State(state): State<MnemoState>, req: Request) -> Result<Marku
|
||||
}
|
||||
}
|
||||
),
|
||||
))
|
||||
)).into_response())
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ pub async fn page(
|
||||
Ok(base(
|
||||
"Persons | Mnemosyne",
|
||||
html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
(nav(&mut conn, Some(&u), req.uri().path()).await)
|
||||
|
||||
div class="mx-auto max-w-4xl px-2 my-4" {
|
||||
p class="flex items-center gap-2" {
|
||||
|
||||
@@ -51,7 +51,7 @@ pub async fn page(
|
||||
Ok(base(
|
||||
&title,
|
||||
html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
(nav(&mut conn, Some(&u), req.uri().path()).await)
|
||||
|
||||
div class="mx-auto max-w-4xl px-2 my-4" {
|
||||
@if let Ok(p) = p {
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub mod add;
|
||||
pub mod id;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PageQuery {
|
||||
@@ -60,7 +61,7 @@ pub async fn page(
|
||||
Ok(base(
|
||||
"Quotes | Mnemosyne",
|
||||
html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
(nav(&mut conn, Some(&u), req.uri().path()).await)
|
||||
|
||||
div class="max-w-4xl mx-auto px-2" {
|
||||
div class="my-4 flex justify-between" {
|
||||
@@ -85,7 +86,7 @@ pub async fn page(
|
||||
}
|
||||
div class="flex flex-col gap-4 mb-8" {
|
||||
@for q in "es {
|
||||
(quote(q))
|
||||
a href=(format!("/quotes/{}", q.id)) class="group/a" {(quote(q))}
|
||||
}
|
||||
|
||||
div class="flex justify-between items-center mt-4 text-neutral-400" {
|
||||
|
||||
@@ -35,11 +35,12 @@ pub async fn page(
|
||||
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
|
||||
};
|
||||
let names = Name::get_all(&mut *conn).await?;
|
||||
let feature_webhooks = state.conf.read().await.discord_webhook.is_some();
|
||||
|
||||
Ok(base(
|
||||
"Add Quote | Mnemosyne",
|
||||
html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
(nav(&mut conn, Some(&u), req.uri().path()).await)
|
||||
|
||||
div class="max-w-4xl mx-auto px-2" {
|
||||
div class="my-4 flex justify-between" {
|
||||
@@ -91,6 +92,15 @@ pub async fn page(
|
||||
class="px-2 py-1 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25";
|
||||
}
|
||||
}
|
||||
@if feature_webhooks {
|
||||
div class="flex flex-col justify-center mt-5" {
|
||||
label class="flex items-center gap-2 cursor-pointer" {
|
||||
input type="checkbox" name="discord_webhook" value="true" checked
|
||||
class="w-4 h-4 cursor-pointer";
|
||||
span {"Send to Discord"}
|
||||
}
|
||||
}
|
||||
}
|
||||
button type="submit" class="border mt-auto mb-2 cursor-pointer rounded h-fit px-2 py-1 bg-neutral-200/5 border-neutral-200/25 hover:border-neutral-200/45 hover:bg-neutral-200/15" {
|
||||
"Submit"
|
||||
}
|
||||
@@ -140,6 +150,7 @@ pub struct IncomingQuote {
|
||||
location: String,
|
||||
time: String,
|
||||
context: String,
|
||||
discord_webhook: Option<String>,
|
||||
}
|
||||
pub async fn form(
|
||||
State(state): State<MnemoState>,
|
||||
@@ -178,5 +189,12 @@ pub async fn form(
|
||||
LogEntry::new(&mut *tx, u, LogAction::CreateQuote { id: q.id }).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
let should_send_webhook = form.discord_webhook.as_deref() == Some("true");
|
||||
if should_send_webhook {
|
||||
if let Some(ref url) = state.conf.read().await.discord_webhook {
|
||||
q.post_msg_webhook(url.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Redirect::to("/dashboard").into_response())
|
||||
}
|
||||
|
||||
146
src/web/pages/quotes/id.rs
Normal file
146
src/web/pages/quotes/id.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use axum::{
|
||||
extract::{Path, Request, State},
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use http::StatusCode;
|
||||
use maud::{PreEscaped, html};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
MnemoState,
|
||||
error::CompositeError,
|
||||
logs::{LogAction, LogEntry},
|
||||
quotes::Quote,
|
||||
users::{
|
||||
User,
|
||||
auth::{UserAuthRequired, UserAuthenticate},
|
||||
permissions::Permission,
|
||||
},
|
||||
web::{
|
||||
components::{nav::nav, quote::quote},
|
||||
icons,
|
||||
pages::base,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn page(
|
||||
State(state): State<MnemoState>,
|
||||
Path(id): Path<Uuid>,
|
||||
req: Request,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let mut conn = state.pool.acquire().await?;
|
||||
let u = match User::authenticate(&mut *conn, req.headers()).await? {
|
||||
Some(u) => u,
|
||||
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
|
||||
};
|
||||
let q = Quote::get_by_id(&mut conn, id).await;
|
||||
let can_delete = u
|
||||
.has_permission(&mut conn, Permission::DeleteQuotes)
|
||||
.await?;
|
||||
|
||||
Ok(base(
|
||||
"Add Quote | Mnemosyne",
|
||||
html!(
|
||||
(nav(&mut conn, Some(&u), req.uri().path()).await)
|
||||
|
||||
div class="max-w-4xl mx-auto px-2" {
|
||||
div class="my-4 flex justify-between" {
|
||||
p class="flex items-center gap-2 text-neutral-500" {
|
||||
(PreEscaped(icons::SCROLL_TEXT))
|
||||
span class="font-lora" {"Quote of ID " (id)}
|
||||
}
|
||||
}
|
||||
@if let Ok(q) = q {
|
||||
(quote(&q))
|
||||
div class="flex flex-row w-full flex-wrap justify-end gap-2 mt-2" {
|
||||
a href="#" disabled class="opacity-[.5] cursor-not-allowed px-2 py-1 border rounded flex flex-row gap-1 bg-neutral-200/5 border-neutral-200/25 hover:bg-neutral-200/15 hover:border-neutral-200/45" {
|
||||
span class="scale-[.75]" {(PreEscaped(icons::PEN))}
|
||||
"Edit"
|
||||
}
|
||||
@if can_delete {
|
||||
a href=(format!("/quotes/{id}/delete")) class="px-2 py-1 cursor-pointer border rounded flex flex-row gap-1 bg-pink-400/10 border-pink-400/25 hover:bg-pink-400/20 hover:border-pink-400/45" {
|
||||
span class="scale-[.75]" {(PreEscaped(icons::TRASH))}
|
||||
"Delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
"Failed to fetch quote. Are you sure it exists?"
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn delete_confirm(
|
||||
State(state): State<MnemoState>,
|
||||
Path(id): Path<Uuid>,
|
||||
req: Request,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let mut conn = state.pool.acquire().await?;
|
||||
let u = match User::authenticate(&mut *conn, req.headers()).await? {
|
||||
Some(u) => u,
|
||||
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
|
||||
};
|
||||
let q = Quote::get_by_id(&mut conn, id).await;
|
||||
|
||||
Ok(base(
|
||||
"Delete Quote | Mnemosyne",
|
||||
html!(
|
||||
(nav(&mut conn, Some(&u), req.uri().path()).await)
|
||||
|
||||
div class="max-w-4xl mx-auto px-2" {
|
||||
div class="my-4 flex justify-between" {
|
||||
p class="flex items-center gap-2 text-neutral-500" {
|
||||
(PreEscaped(icons::TRASH))
|
||||
span class="font-lora" {"Deleting quote of ID " (id)}
|
||||
}
|
||||
}
|
||||
@if let Ok(q) = q {
|
||||
div class="border border-pink-400/25 bg-pink-400/10 rounded-md p-3 mb-4" {
|
||||
p class="flex flex-wrap items-center gap-2 text-pink-200" {
|
||||
span class="font-semibold" {"Are you sure you want to delete this quote?"}
|
||||
span class="text-pink-300/80" {"This cannot be undone."}
|
||||
}
|
||||
}
|
||||
(quote(&q))
|
||||
div class="flex flex-row w-full flex-wrap justify-start gap-2 mt-2" {
|
||||
form method="post" action=(format!("/quotes/{id}/delete")) {
|
||||
button type="submit" class="px-2 py-1 cursor-pointer border rounded flex flex-row gap-1 bg-pink-400/10 border-pink-400/25 hover:bg-pink-400/20 hover:border-pink-400/45" {
|
||||
span class="scale-[.75]" {(PreEscaped(icons::TRASH))}
|
||||
"Delete"
|
||||
}
|
||||
}
|
||||
a href=(format!("/quotes/{id}")) class="px-2 py-1 border rounded flex flex-row gap-1 bg-neutral-200/5 border-neutral-200/25 hover:bg-neutral-200/15 hover:border-neutral-200/45" {
|
||||
"Cancel"
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
"Failed to fetch quote. Are you sure it exists?"
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn delete(
|
||||
State(state): State<MnemoState>,
|
||||
Path(id): Path<Uuid>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let mut tx = state.pool.begin().await?;
|
||||
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
|
||||
if !u.has_permission(&mut tx, Permission::DeleteQuotes).await? {
|
||||
return Ok((StatusCode::FORBIDDEN, "No permission.").into_response());
|
||||
}
|
||||
|
||||
let q = Quote::get_by_id(&mut *tx, id).await?;
|
||||
LogEntry::new(&mut *tx, u, LogAction::DeleteQuote { quote: q.clone() }).await?;
|
||||
q.delete(&mut tx).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Redirect::to("/quotes").into_response())
|
||||
}
|
||||
@@ -46,7 +46,7 @@ pub async fn page(
|
||||
Ok(base(
|
||||
"Tags | Mnemosyne",
|
||||
html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
(nav(&mut conn, Some(&u), req.uri().path()).await)
|
||||
|
||||
div class="mx-auto max-w-4xl px-2 my-4" {
|
||||
p class="flex items-center gap-2" {
|
||||
|
||||
@@ -3,6 +3,7 @@ use axum::{
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use maud::{PreEscaped, html};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
MnemoState,
|
||||
@@ -27,7 +28,14 @@ pub async fn page(
|
||||
Some(u) => u,
|
||||
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
|
||||
};
|
||||
let us = User::get_all(&mut *conn).await;
|
||||
let us = User::get_all(&mut *conn).await.map(|mut v| {
|
||||
v.sort_by_key(|p| match p.id {
|
||||
id if id == Uuid::nil() => (0, p.id),
|
||||
id if id == Uuid::max() => (1, p.id),
|
||||
_ => (2, p.id),
|
||||
});
|
||||
v
|
||||
});
|
||||
let can_create_users = u
|
||||
.has_permission(&mut *conn, Permission::ManuallyCreateUsers)
|
||||
.await;
|
||||
@@ -35,7 +43,7 @@ pub async fn page(
|
||||
Ok(base(
|
||||
"Users | Mnemosyne",
|
||||
html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
(nav(&mut conn, Some(&u), req.uri().path()).await)
|
||||
|
||||
div class="mx-auto max-w-4xl px-2 my-4" {
|
||||
p class="flex items-center gap-2" {
|
||||
|
||||
@@ -36,7 +36,7 @@ pub async fn page(
|
||||
Ok(base(
|
||||
"Create User | Mnemosyne",
|
||||
html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
(nav(&mut conn, Some(&u), req.uri().path()).await)
|
||||
|
||||
div class="mx-auto max-w-4xl px-2 my-4" {
|
||||
p class="flex items-center gap-2" {
|
||||
|
||||
@@ -33,7 +33,7 @@ pub async fn page(
|
||||
return Ok(base(
|
||||
"No such user | Mnemosyne",
|
||||
html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
(nav(&mut tx, Some(&u), req.uri().path()).await)
|
||||
div class="mx-auto max-w-4xl mt-16 text-center" {
|
||||
div class="text-6xl mb-4" { "?" }
|
||||
p class="text-red-400 text-lg" { "No such user found." }
|
||||
@@ -48,7 +48,7 @@ pub async fn page(
|
||||
}
|
||||
_ => {
|
||||
return Ok(base("Error | Mnemosyne", html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
(nav(&mut tx, Some(&u), req.uri().path()).await)
|
||||
p class="text-red-400 text-center my-4" { "An error occurred while loading this profile." }
|
||||
)).into_response());
|
||||
}
|
||||
@@ -70,7 +70,7 @@ pub async fn page(
|
||||
Ok(base(
|
||||
&format!("@{} | Mnemosyne", user.handle),
|
||||
html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
(nav(&mut tx, Some(&u), req.uri().path()).await)
|
||||
|
||||
// banner
|
||||
div class="relative w-full h-48 sm:h-56 md:h-64 bg-linear-to-b from-neutral-800 from-25% to-emerald-950 overflow-hidden" {
|
||||
|
||||
@@ -32,18 +32,16 @@ pub async fn page(
|
||||
Ok(base(
|
||||
"User Settings | Mnemosyne",
|
||||
html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
(nav(&mut conn, Some(&u), req.uri().path()).await)
|
||||
|
||||
div class="max-w-4xl mx-auto px-2" {
|
||||
div class="mx-auto max-w-4xl my-4" {
|
||||
p class="flex items-center gap-2" {
|
||||
span class="text-neutral-500" {(PreEscaped(icons::SERVER))}
|
||||
span class="text-neutral-500" {(PreEscaped(icons::SETTINGS))}
|
||||
span class="text-2xl font-semibold font-lora" {"Your User Settings"}
|
||||
}
|
||||
p class="text-neutral-500 text-sm font-light" {
|
||||
// "Hi, " (u.handle) "!" " " "This is your user settings page." br;
|
||||
"Looking for Mnemosyne settings?" " "
|
||||
a class="text-blue-500 hover:text-blue-400 hover:underline" href="/mnemosyne-settings" {"Here."}
|
||||
"Hi, " (u.handle) "!" " " "This is your user settings page."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user