Added web ui with templates
This commit is contained in:
parent
d2cb5b3031
commit
90b02eef79
24 changed files with 1403 additions and 78 deletions
328
Cargo.lock
generated
328
Cargo.lock
generated
|
|
@ -212,6 +212,16 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.14.0"
|
version = "3.14.0"
|
||||||
|
|
@ -260,6 +270,28 @@ dependencies = [
|
||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono-tz"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz-build",
|
||||||
|
"phf",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono-tz-build"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f"
|
||||||
|
dependencies = [
|
||||||
|
"parse-zoneinfo",
|
||||||
|
"phf",
|
||||||
|
"phf_codegen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
|
|
@ -296,6 +328,25 @@ version = "2.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-queue"
|
name = "crossbeam-queue"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
|
|
@ -308,12 +359,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.16"
|
version = "0.8.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
|
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
|
|
@ -336,6 +384,12 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deunicode"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ae2a35373c5c74340b79ae6780b498b2b183915ec5dacf263aac5a099bf485a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
|
|
@ -561,6 +615,30 @@ version = "0.28.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
|
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globset"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"bstr",
|
||||||
|
"log",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globwalk"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"ignore",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.2"
|
version = "0.14.2"
|
||||||
|
|
@ -686,6 +764,15 @@ version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humansize"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
||||||
|
dependencies = [
|
||||||
|
"libm",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.27"
|
version = "0.14.27"
|
||||||
|
|
@ -742,6 +829,22 @@ dependencies = [
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ignore"
|
||||||
|
version = "0.4.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"globset",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"same-file",
|
||||||
|
"walkdir",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
|
@ -990,6 +1093,15 @@ dependencies = [
|
||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parse-zoneinfo"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
|
||||||
|
dependencies = [
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
|
|
@ -1011,6 +1123,89 @@ version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest"
|
||||||
|
version = "2.7.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"thiserror",
|
||||||
|
"ucd-trie",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_derive"
|
||||||
|
version = "2.7.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22e1288dbd7786462961e69bfd4df7848c1e37e8b74303dbdab82c3a9cdd2809"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_generator",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_generator"
|
||||||
|
version = "2.7.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1381c29a877c6d34b8c176e734f35d7f7f5b3adaefe940cb4d1bb7af94678e2e"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_meta",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.39",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_meta"
|
||||||
|
version = "2.7.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0934d6907f148c22a3acbda520c7eed243ad7487a30f51f6ce52b58b7077a8a"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"pest",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_codegen"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared",
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
|
|
@ -1213,6 +1408,15 @@ version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
@ -1313,6 +1517,12 @@ dependencies = [
|
||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
|
|
@ -1322,6 +1532,16 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slug"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4"
|
||||||
|
dependencies = [
|
||||||
|
"deunicode",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.11.2"
|
version = "1.11.2"
|
||||||
|
|
@ -1641,6 +1861,28 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tera"
|
||||||
|
version = "1.19.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
|
"globwalk",
|
||||||
|
"humansize",
|
||||||
|
"lazy_static",
|
||||||
|
"percent-encoding",
|
||||||
|
"pest",
|
||||||
|
"pest_derive",
|
||||||
|
"rand",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"slug",
|
||||||
|
"unic-segment",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.50"
|
version = "1.0.50"
|
||||||
|
|
@ -1789,6 +2031,62 @@ version = "1.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ucd-trie"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-char-property"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
|
||||||
|
dependencies = [
|
||||||
|
"unic-char-range",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-char-range"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-common"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-segment"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
|
||||||
|
dependencies = [
|
||||||
|
"unic-ucd-segment",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-ucd-segment"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
|
||||||
|
dependencies = [
|
||||||
|
"unic-char-property",
|
||||||
|
"unic-char-range",
|
||||||
|
"unic-ucd-version",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-ucd-version"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
|
||||||
|
dependencies = [
|
||||||
|
"unic-common",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
|
|
@ -1845,6 +2143,16 @@ version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
@ -1925,6 +2233,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"tera",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1950,6 +2259,15 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
|
||||||
3
base.css
Normal file
3
base.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
14
build.rs
Normal file
14
build.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use std::env::current_dir;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
Command::new("npx")
|
||||||
|
.args(&["tailwindcss", "-i", "base.css", "-o"])
|
||||||
|
.arg(&format!(
|
||||||
|
"{}/static/styles.css",
|
||||||
|
current_dir().unwrap().into_os_string().to_str().unwrap()
|
||||||
|
))
|
||||||
|
.status()
|
||||||
|
.unwrap();
|
||||||
|
println!("cargo:rerun-if-changed=templates/*")
|
||||||
|
}
|
||||||
|
|
@ -195,7 +195,7 @@ impl AccountSnapshot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromRow, Serialize, Deserialize)]
|
#[derive(FromRow, Serialize, Deserialize, Debug)]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
account_id: i32,
|
account_id: i32,
|
||||||
user: i32,
|
user: i32,
|
||||||
|
|
|
||||||
591
static/styles.css
Normal file
591
static/styles.css
Normal file
|
|
@ -0,0 +1,591 @@
|
||||||
|
/*
|
||||||
|
! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||||
|
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||||
|
*/
|
||||||
|
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 1 */
|
||||||
|
border-width: 0;
|
||||||
|
/* 2 */
|
||||||
|
border-style: solid;
|
||||||
|
/* 2 */
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
--tw-content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use a consistent sensible line-height in all browsers.
|
||||||
|
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
3. Use a more readable tab size.
|
||||||
|
4. Use the user's configured `sans` font-family by default.
|
||||||
|
5. Use the user's configured `sans` font-feature-settings by default.
|
||||||
|
6. Use the user's configured `sans` font-variation-settings by default.
|
||||||
|
7. Disable tap highlights on iOS
|
||||||
|
*/
|
||||||
|
|
||||||
|
html,
|
||||||
|
:host {
|
||||||
|
line-height: 1.5;
|
||||||
|
/* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
/* 2 */
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
/* 3 */
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
/* 3 */
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
/* 4 */
|
||||||
|
font-feature-settings: normal;
|
||||||
|
/* 5 */
|
||||||
|
font-variation-settings: normal;
|
||||||
|
/* 6 */
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
/* 7 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove the margin in all browsers.
|
||||||
|
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
/* 1 */
|
||||||
|
line-height: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Add the correct height in Firefox.
|
||||||
|
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||||
|
3. Ensure horizontal rules are visible by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 0;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
border-top-width: 1px;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr:where([title]) {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the default font size and weight for headings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Reset links to optimize for opt-in styling instead of opt-out.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font weight in Edge and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use the user's configured `mono` font-family by default.
|
||||||
|
2. Use the user's configured `mono` font-feature-settings by default.
|
||||||
|
3. Use the user's configured `mono` font-variation-settings by default.
|
||||||
|
4. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp,
|
||||||
|
pre {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
/* 1 */
|
||||||
|
font-feature-settings: normal;
|
||||||
|
/* 2 */
|
||||||
|
font-variation-settings: normal;
|
||||||
|
/* 3 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||||
|
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||||
|
3. Remove gaps between table borders by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
text-indent: 0;
|
||||||
|
/* 1 */
|
||||||
|
border-color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
border-collapse: collapse;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Change the font styles in all browsers.
|
||||||
|
2. Remove the margin in Firefox and Safari.
|
||||||
|
3. Remove default padding in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-feature-settings: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-variation-settings: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 100%;
|
||||||
|
/* 1 */
|
||||||
|
font-weight: inherit;
|
||||||
|
/* 1 */
|
||||||
|
line-height: inherit;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 1 */
|
||||||
|
margin: 0;
|
||||||
|
/* 2 */
|
||||||
|
padding: 0;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inheritance of text transform in Edge and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Remove default button styles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type='button'],
|
||||||
|
[type='reset'],
|
||||||
|
[type='submit'] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
background-color: transparent;
|
||||||
|
/* 2 */
|
||||||
|
background-image: none;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Use the modern Firefox focus style for all focusable elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-focusring {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-ui-invalid {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct vertical alignment in Chrome and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Correct the cursor style of increment and decrement buttons in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button,
|
||||||
|
::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type='search'] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
/* 1 */
|
||||||
|
outline-offset: -2px;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
font: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct display in Chrome and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Removes the default spacing and border for appropriate elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
dl,
|
||||||
|
dd,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
hr,
|
||||||
|
figure,
|
||||||
|
p,
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
menu {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Reset default styling for dialogs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent resizing textareas horizontally by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||||
|
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
/* 1 */
|
||||||
|
color: #9ca3af;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
/* 1 */
|
||||||
|
color: #9ca3af;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set the default cursor for buttons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Make sure disabled buttons don't get the pointer cursor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||||
|
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
audio,
|
||||||
|
iframe,
|
||||||
|
embed,
|
||||||
|
object {
|
||||||
|
display: block;
|
||||||
|
/* 1 */
|
||||||
|
vertical-align: middle;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, ::before, ::after {
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
}
|
||||||
|
|
||||||
|
::backdrop {
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-full {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-grow {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grow {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border {
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-stone-300 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(214 211 209 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-4 {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-stone-400:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(168 162 158 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
9
tailwind.config.js
Normal file
9
tailwind.config.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./templates/**.html"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
||||||
25
templates/accounts.html
Normal file
25
templates/accounts.html
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Account {{account.account_name}}{% endblock title %}
|
||||||
|
{% block body %}
|
||||||
|
<div>{{account.account_name}}</div>
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Descripción</th>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>Cantidad</th>
|
||||||
|
<th>Categoría</th>
|
||||||
|
</tr>
|
||||||
|
{% for tx in transactions %}
|
||||||
|
<tr>
|
||||||
|
<td>{{tx.description}}</td>
|
||||||
|
<td>{{tx.transaction_timestamp}}</td>
|
||||||
|
<td>{{tx.amount/100}}</td>
|
||||||
|
<td>{{tx.category}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
<p>Cargados {{n_txs}} movimientos</p>
|
||||||
|
</div>
|
||||||
|
{% endblock body %}
|
||||||
|
|
||||||
38
templates/base.html
Normal file
38
templates/base.html
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}{% endblock title %}</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/styles.css">
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
background:
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
max-width: 12rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar a {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="flex h-full">
|
||||||
|
<aside class="sidebar bg-stone-300 p-4 flex flex-col">
|
||||||
|
<a class="hover:bg-stone-400" href="/">Inicio</a>
|
||||||
|
<a class="hover:bg-stone-400" href="/rules">Reglas</a>
|
||||||
|
</aside>
|
||||||
|
<div class="p-4 grow">
|
||||||
|
{% block body %}
|
||||||
|
{% endblock body %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
templates/index.html
Normal file
7
templates/index.html
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Index{% endblock title %}
|
||||||
|
{% block body %}
|
||||||
|
{% for account in accounts %}
|
||||||
|
<a href="/accounts/id/{{account.account_id}}">{{account.account_name}}({{account.account_id}})</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock body %}
|
||||||
23
templates/rules_list.html
Normal file
23
templates/rules_list.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Rules{% endblock title %}
|
||||||
|
{% block body %}
|
||||||
|
<div>
|
||||||
|
<a href="/rules/new">New</a>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Categoría</th>
|
||||||
|
<th>Regla</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for rule in rules %}
|
||||||
|
<tr>
|
||||||
|
<td>{{rule.category}}</td>
|
||||||
|
<td>{{rule.regex}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock body %}
|
||||||
36
templates/rules_new.html
Normal file
36
templates/rules_new.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Create rule{% endblock title %}
|
||||||
|
{% block body %}
|
||||||
|
<form action="/rules/new" method="post" class="flex flex-col">
|
||||||
|
<label class="grow">
|
||||||
|
Description
|
||||||
|
<input type="text" name="description" />
|
||||||
|
</label>
|
||||||
|
<label class="grow">
|
||||||
|
Regex
|
||||||
|
<input type="text" name="regex" />
|
||||||
|
</label>
|
||||||
|
<label class="grow">
|
||||||
|
Category
|
||||||
|
<select name="category">
|
||||||
|
<option></option>
|
||||||
|
{% for cat in categories %}
|
||||||
|
<option value={{cat.category_id}}>{{cat.name}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
<style>
|
||||||
|
label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.25rem;
|
||||||
|
margin: 0.25rem;
|
||||||
|
}
|
||||||
|
{% endblock body %}
|
||||||
12
templates/rules_new_success.html
Normal file
12
templates/rules_new_success.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Rule created</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta http-equiv="refresh" content="3;url=.." />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Rule created successfully
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -10,7 +10,8 @@ serde = { workspace = true, features = ["derive"] }
|
||||||
chrono = { workspace = true, features = ["serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
sqlx = { workspace = true, features = ["runtime-tokio", "sqlite", "chrono"]}
|
sqlx = { workspace = true, features = ["runtime-tokio", "sqlite", "chrono"]}
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
axum = { version = "0.6.20", features = ["macros", "headers"] }
|
axum = { version = "0.6.20", features = ["macros", "headers", "form"] }
|
||||||
hyper = "0.14.27"
|
hyper = "0.14.27"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
accounters = { path = ".." }
|
accounters = { path = ".." }
|
||||||
|
tera = "1.19.1"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use axum::{
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
|
use tera::Tera;
|
||||||
|
|
||||||
mod routes;
|
mod routes;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
@ -19,38 +20,56 @@ const DB_URL: &str = "sqlite://sqlite.db";
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let db = accounters::create_db(DB_URL).await.unwrap();
|
let db = accounters::create_db(DB_URL).await.unwrap();
|
||||||
|
|
||||||
let state = AppState { db: Arc::new(db) };
|
let mut tmpls = Tera::new("templates/*").unwrap();
|
||||||
|
tmpls.autoescape_on(vec!["html"]);
|
||||||
|
|
||||||
|
let state = AppState {
|
||||||
|
db: Arc::new(db),
|
||||||
|
tmpls: Arc::new(tmpls),
|
||||||
|
};
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(index))
|
.nest(
|
||||||
|
"/",
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(routes::ui::index))
|
||||||
|
.route("/accounts/id/:id", get(routes::ui::account))
|
||||||
|
.route("/rules", get(routes::ui::rules::list))
|
||||||
|
.route("/rules/new", get(routes::ui::rules::new_view))
|
||||||
|
.route("/rules/new", post(routes::ui::rules::new_action))
|
||||||
|
.nest(
|
||||||
|
"/static",
|
||||||
|
Router::new().route("/styles.css", get(routes::static_routes::styles)),
|
||||||
|
),
|
||||||
|
)
|
||||||
.nest(
|
.nest(
|
||||||
"/api/v1",
|
"/api/v1",
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/user", post(routes::create_user))
|
.route("/user", post(routes::api::create_user))
|
||||||
.route("/login", post(routes::login))
|
.route("/login", post(routes::api::login))
|
||||||
.route("/accounts", post(routes::accounts::account_create))
|
.route("/accounts", post(routes::api::accounts::account_create))
|
||||||
.route("/accounts", get(routes::accounts::account_list))
|
.route("/accounts", get(routes::api::accounts::account_list))
|
||||||
.route("/accounts/id/:id", get(routes::accounts::account_get))
|
.route("/accounts/id/:id", get(routes::api::accounts::account_get))
|
||||||
.route(
|
.route(
|
||||||
"/accounts/id/:id/transaction",
|
"/accounts/id/:id/transaction",
|
||||||
post(routes::transactions::create),
|
post(routes::api::transactions::create),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/accounts/id/:id/transaction",
|
"/accounts/id/:id/transaction",
|
||||||
get(routes::transactions::list),
|
get(routes::api::transactions::list),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/accounts/id/:id/update",
|
"/accounts/id/:id/update",
|
||||||
post(routes::accounts::snapshot_update),
|
post(routes::api::accounts::snapshot_update),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/accounts/id/:id/recategorize",
|
"/accounts/id/:id/recategorize",
|
||||||
post(routes::accounts::recategorize),
|
post(routes::api::accounts::recategorize),
|
||||||
)
|
)
|
||||||
.route("/categories", post(routes::categories::create))
|
.route("/categories", post(routes::api::categories::create))
|
||||||
.route("/categories", get(routes::categories::list))
|
.route("/categories", get(routes::api::categories::list))
|
||||||
.route("/rules", post(routes::rules::create))
|
.route("/rules", post(routes::api::rules::create))
|
||||||
.route("/rules", get(routes::rules::list)),
|
.route("/rules", get(routes::api::rules::list)),
|
||||||
)
|
)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
|
|
@ -64,6 +83,7 @@ async fn main() {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
db: Arc<SqlitePool>,
|
db: Arc<SqlitePool>,
|
||||||
|
tmpls: Arc<Tera>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromRef<AppState> for Arc<SqlitePool> {
|
impl FromRef<AppState> for Arc<SqlitePool> {
|
||||||
|
|
@ -72,6 +92,12 @@ impl FromRef<AppState> for Arc<SqlitePool> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromRef<AppState> for Arc<Tera> {
|
||||||
|
fn from_ref(state: &AppState) -> Arc<Tera> {
|
||||||
|
state.tmpls.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn index() -> (StatusCode, String) {
|
async fn index() -> (StatusCode, String) {
|
||||||
(StatusCode::OK, String::from("Hello, World!"))
|
(StatusCode::OK, String::from("Hello, World!"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,3 @@
|
||||||
use std::sync::Arc;
|
pub mod api;
|
||||||
|
pub mod static_routes;
|
||||||
use axum::extract::{Json, State};
|
pub mod ui;
|
||||||
use hyper::StatusCode;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::SqlitePool;
|
|
||||||
|
|
||||||
use accounters::models::users::User;
|
|
||||||
|
|
||||||
pub mod accounts;
|
|
||||||
pub mod categories;
|
|
||||||
pub mod rules;
|
|
||||||
pub mod transactions;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct CreateUserRequest {
|
|
||||||
user: String,
|
|
||||||
pass: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_user(
|
|
||||||
State(db): State<Arc<SqlitePool>>,
|
|
||||||
Json(user_info): Json<CreateUserRequest>,
|
|
||||||
) -> (StatusCode, String) {
|
|
||||||
let exec = User::create_user(db.as_ref(), &user_info.user, &user_info.pass).await;
|
|
||||||
match exec {
|
|
||||||
Ok(e) => (StatusCode::OK, format!("{}", e.get_id())),
|
|
||||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn login(
|
|
||||||
State(db): State<Arc<SqlitePool>>,
|
|
||||||
Json(user_info): Json<CreateUserRequest>,
|
|
||||||
) -> (StatusCode, String) {
|
|
||||||
let user = User::get_user(db.as_ref(), &user_info.user).await.unwrap();
|
|
||||||
|
|
||||||
if user.check_pass(&user_info.pass) {
|
|
||||||
(StatusCode::OK, format!("{}", user.get_id()))
|
|
||||||
} else {
|
|
||||||
(StatusCode::UNAUTHORIZED, String::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
43
webserver/src/routes/api.rs
Normal file
43
webserver/src/routes/api.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::extract::{Json, State};
|
||||||
|
use hyper::StatusCode;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
use accounters::models::users::User;
|
||||||
|
|
||||||
|
pub mod accounts;
|
||||||
|
pub mod categories;
|
||||||
|
pub mod rules;
|
||||||
|
pub mod transactions;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateUserRequest {
|
||||||
|
user: String,
|
||||||
|
pass: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_user(
|
||||||
|
State(db): State<Arc<SqlitePool>>,
|
||||||
|
Json(user_info): Json<CreateUserRequest>,
|
||||||
|
) -> (StatusCode, String) {
|
||||||
|
let exec = User::create_user(db.as_ref(), &user_info.user, &user_info.pass).await;
|
||||||
|
match exec {
|
||||||
|
Ok(e) => (StatusCode::OK, format!("{}", e.get_id())),
|
||||||
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
State(db): State<Arc<SqlitePool>>,
|
||||||
|
Json(user_info): Json<CreateUserRequest>,
|
||||||
|
) -> (StatusCode, String) {
|
||||||
|
let user = User::get_user(db.as_ref(), &user_info.user).await.unwrap();
|
||||||
|
|
||||||
|
if user.check_pass(&user_info.pass) {
|
||||||
|
(StatusCode::OK, format!("{}", user.get_id()))
|
||||||
|
} else {
|
||||||
|
(StatusCode::UNAUTHORIZED, String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
12
webserver/src/routes/static_routes.rs
Normal file
12
webserver/src/routes/static_routes.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use hyper::{header::CONTENT_TYPE, StatusCode};
|
||||||
|
|
||||||
|
pub async fn styles() -> impl IntoResponse {
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
[(CONTENT_TYPE, "text/css")],
|
||||||
|
fs::read_to_string("static/styles.css").unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
102
webserver/src/routes/ui.rs
Normal file
102
webserver/src/routes/ui.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use hyper::{header::CONTENT_TYPE, StatusCode};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
use tera::{Context, Tera};
|
||||||
|
|
||||||
|
use crate::users::UserToken;
|
||||||
|
use accounters::models::{Account, Transaction};
|
||||||
|
|
||||||
|
pub mod rules;
|
||||||
|
|
||||||
|
pub async fn index(
|
||||||
|
State(db): State<Arc<SqlitePool>>,
|
||||||
|
State(tmpls): State<Arc<Tera>>,
|
||||||
|
uid: UserToken,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let mut ctx = Context::new();
|
||||||
|
|
||||||
|
let accounts = Account::list(db.as_ref(), uid.user_id).await.unwrap();
|
||||||
|
ctx.insert("accounts", &accounts);
|
||||||
|
|
||||||
|
match tmpls.render("index.html", &ctx) {
|
||||||
|
Ok(out) => (
|
||||||
|
StatusCode::OK,
|
||||||
|
[(CONTENT_TYPE, "text/html;charset=utf-8")],
|
||||||
|
out,
|
||||||
|
),
|
||||||
|
Err(e) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
[(CONTENT_TYPE, "text/plain")],
|
||||||
|
format!("{e}"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct AccountViewParams {
|
||||||
|
movements: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn account(
|
||||||
|
State(db): State<Arc<SqlitePool>>,
|
||||||
|
State(tmpls): State<Arc<Tera>>,
|
||||||
|
uid: UserToken,
|
||||||
|
Path(account_id): Path<i32>,
|
||||||
|
Query(AccountViewParams { movements }): Query<AccountViewParams>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let mut ctx = Context::new();
|
||||||
|
|
||||||
|
let account = match Account::get_by_id(db.as_ref(), account_id).await {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(e) => {
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
[(CONTENT_TYPE, "text/plain")],
|
||||||
|
format!("{e}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if account.get_user() != uid.user_id {
|
||||||
|
return (
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
[(CONTENT_TYPE, "text/plain")],
|
||||||
|
String::from("You cannot access this resource"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let txs = match Transaction::list(
|
||||||
|
db.as_ref(),
|
||||||
|
account.get_id(),
|
||||||
|
movements.unwrap_or(10),
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
[(CONTENT_TYPE, "text/plain")],
|
||||||
|
format!("Error at loading transactions: {e}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.insert("account", &account);
|
||||||
|
ctx.insert("transactions", &txs);
|
||||||
|
ctx.insert("n_txs", &txs.len());
|
||||||
|
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
[(CONTENT_TYPE, "text/html;charset=utf-8")],
|
||||||
|
tmpls.render("accounts.html", &ctx).unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
84
webserver/src/routes/ui/rules.rs
Normal file
84
webserver/src/routes/ui/rules.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use accounters::models::{categories::Category, rules::Rule};
|
||||||
|
use axum::{
|
||||||
|
extract::{Form, State},
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use hyper::{header::CONTENT_TYPE, StatusCode};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
use tera::{Context, Tera};
|
||||||
|
|
||||||
|
use crate::users::UserToken;
|
||||||
|
|
||||||
|
pub async fn list(
|
||||||
|
State(db): State<Arc<SqlitePool>>,
|
||||||
|
State(tmpls): State<Arc<Tera>>,
|
||||||
|
uid: UserToken,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let rules = match Rule::list_by_user(db.as_ref(), uid.user_id).await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
[(CONTENT_TYPE, "text/plain")],
|
||||||
|
format!("{e:?}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ctx = Context::new();
|
||||||
|
|
||||||
|
ctx.insert("rules", &rules);
|
||||||
|
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
[(CONTENT_TYPE, "text/html;charset=utf-8")],
|
||||||
|
tmpls.render("rules_list.html", &ctx).unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_view(
|
||||||
|
State(db): State<Arc<SqlitePool>>,
|
||||||
|
State(tmpls): State<Arc<Tera>>,
|
||||||
|
uid: UserToken,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let categories = Category::list(db.as_ref()).await.unwrap();
|
||||||
|
let mut ctx = Context::new();
|
||||||
|
ctx.insert("categories", &categories);
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
[(CONTENT_TYPE, "text/html;charset=utf-8")],
|
||||||
|
tmpls.render("rules_new.html", &ctx).unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct NewRuleParams {
|
||||||
|
pub description: String,
|
||||||
|
pub regex: String,
|
||||||
|
pub category: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_action(
|
||||||
|
State(db): State<Arc<SqlitePool>>,
|
||||||
|
State(tmpls): State<Arc<Tera>>,
|
||||||
|
uid: UserToken,
|
||||||
|
Form(params): Form<NewRuleParams>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match Rule::new(db.as_ref(), uid.user_id, params.regex, params.category).await {
|
||||||
|
Ok(_) => (
|
||||||
|
StatusCode::OK,
|
||||||
|
[(CONTENT_TYPE, "text/html;charset=utf-8")],
|
||||||
|
tmpls
|
||||||
|
.render("rules_new_success.html", &Context::new())
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
Err(e) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
[(CONTENT_TYPE, "text/plain;charset=utf-8")],
|
||||||
|
format!("{e}"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use accounters::models::users::User;
|
||||||
use axum::{
|
use axum::{
|
||||||
async_trait,
|
async_trait,
|
||||||
extract::FromRequestParts,
|
extract::{FromRef, FromRequestParts},
|
||||||
headers::authorization::Bearer,
|
headers::authorization::{Basic, Bearer},
|
||||||
headers::Authorization,
|
headers::Authorization,
|
||||||
http::request::Parts,
|
http::request::Parts,
|
||||||
response::{IntoResponse, Redirect},
|
response::{IntoResponse, Redirect},
|
||||||
RequestPartsExt, TypedHeader,
|
RequestPartsExt, TypedHeader,
|
||||||
};
|
};
|
||||||
|
use hyper::StatusCode;
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
pub struct AuthRedirect;
|
pub struct AuthRedirect;
|
||||||
|
|
||||||
|
|
@ -23,20 +29,35 @@ pub struct UserToken {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<S> FromRequestParts<S> for UserToken
|
impl<S> FromRequestParts<S> for UserToken
|
||||||
where
|
where
|
||||||
|
AppState: FromRef<S>,
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
{
|
{
|
||||||
type Rejection = AuthRedirect;
|
type Rejection = (StatusCode, [(&'static str, &'static str); 1]);
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
let auth = parts
|
match parts.extract::<TypedHeader<Authorization<Bearer>>>().await {
|
||||||
.extract::<TypedHeader<Authorization<Bearer>>>()
|
Ok(auth) => Ok(UserToken {
|
||||||
.await
|
user_id: auth.0 .0.token().parse().unwrap(),
|
||||||
.map_err(|e| panic!("Could not get cookies: {e}"))
|
}),
|
||||||
.unwrap();
|
Err(_) => match parts.extract::<TypedHeader<Authorization<Basic>>>().await {
|
||||||
let ut = UserToken {
|
Ok(auth) => {
|
||||||
user_id: auth.0 .0.token().parse().unwrap(),
|
let state = AppState::from_ref(state);
|
||||||
};
|
let user = User::get_user(state.db.as_ref(), auth.username())
|
||||||
Ok(ut)
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if user.check_pass(auth.password()) {
|
||||||
|
Ok(UserToken {
|
||||||
|
user_id: user.get_id(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err((StatusCode::UNAUTHORIZED, [("", "")]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
[("WWW-Authenticate", "Basic realm=\"Access\"")],
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue