Working user and link list, (big)restructure
This is now a workspace consisting of: * the pslink app (a wasm frontend for the admin interface) * the pslink binary * shared - modules for communication between the two above.
This commit is contained in:
parent
1611cfb9a2
commit
fc9b18141f
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,3 +5,5 @@ launch.json
|
||||
settings.json
|
||||
links.session.sql
|
||||
sqltemplates
|
||||
dist
|
||||
pkg
|
413
Cargo.lock
generated
413
Cargo.lock
generated
@ -45,6 +45,26 @@ dependencies = [
|
||||
"trust-dns-resolver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-files"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c51e8a9146c12fce92a6e4c24b8c4d9b05268130bfd8d61bc587e822c32ce689"
|
||||
dependencies = [
|
||||
"actix-service",
|
||||
"actix-web",
|
||||
"bitflags",
|
||||
"bytes 0.5.6",
|
||||
"derive_more",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"v_htmlescape",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "2.2.0"
|
||||
@ -81,7 +101,7 @@ dependencies = [
|
||||
"log",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project 1.0.6",
|
||||
"pin-project 1.0.7",
|
||||
"rand 0.7.3",
|
||||
"regex",
|
||||
"serde",
|
||||
@ -276,7 +296,7 @@ dependencies = [
|
||||
"fxhash",
|
||||
"log",
|
||||
"mime",
|
||||
"pin-project 1.0.6",
|
||||
"pin-project 1.0.7",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -437,6 +457,20 @@ version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"enum-map",
|
||||
"fluent 0.15.0",
|
||||
"seed",
|
||||
"serde",
|
||||
"shared",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.2.0"
|
||||
@ -474,9 +508,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.48"
|
||||
version = "0.1.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36ea56748e10732c49404c153638a15ec3d6211ec5ff35d9bb20e13b93576adf"
|
||||
checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.26",
|
||||
"quote 1.0.9",
|
||||
@ -541,9 +575,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.56"
|
||||
version = "0.3.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc"
|
||||
checksum = "78ed203b9ba68b242c62b3fb7480f589dd49829be1edb3fe8fc8b4ffda2dcb8d"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if 1.0.0",
|
||||
@ -680,10 +714,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build_const"
|
||||
version = "0.2.1"
|
||||
name = "buf-min"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
|
||||
checksum = "fa17aa1cf56bdd6bb30518767d00e58019d326f3f05d8c3e0730b549d332ea83"
|
||||
dependencies = [
|
||||
"bytes 0.5.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build_const"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
@ -863,10 +906,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "const_fn"
|
||||
version = "0.4.6"
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "076a6803b0dacd6a88cfe64deba628b01533ff5ef265687e6938280c1afd0a28"
|
||||
checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const_fn"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402da840495de3f976eaefc3485b7f5eb5b0bf9761f9a47be27fe975b3b8c2ec"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
@ -1033,6 +1086,15 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbg"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4677188513e0e9d7adced5997cf9a1e7a3c996c994f90093325c5332c1a8b221"
|
||||
dependencies = [
|
||||
"version_check 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
version = "0.8.6"
|
||||
@ -1106,6 +1168,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enclose"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1056f553da426e9c025a662efa48b52e62e0a3a7648aa2d15aeaaf7f0d329357"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.28"
|
||||
@ -1127,6 +1195,27 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-map"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88acdb627a242ba1bf36653fa200f72c037ca3324e0710d1ac4fee809a1539cd"
|
||||
dependencies = [
|
||||
"enum-map-derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-map-derive"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2dccd930f0c0a8968d873b10d3611f71bffc4dff84879dabf7b746b0686ea81"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.26",
|
||||
"quote 1.0.9",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.6.2"
|
||||
@ -1186,7 +1275,17 @@ version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960ac6317b829b94c67f9a774e8b56db388405e174855a5a84d4b461ff85b281"
|
||||
dependencies = [
|
||||
"fluent-bundle",
|
||||
"fluent-bundle 0.14.4",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc4d7142005e2066e4844caf9f271b93fc79836ee96ec85057b8c109687e629a"
|
||||
dependencies = [
|
||||
"fluent-bundle 0.15.1",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
@ -1197,10 +1296,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3cc2d1c59a0daaa93bb346db97e1ebad1067c5ffedc1af8b937a9d8caa6a77"
|
||||
dependencies = [
|
||||
"fluent-langneg",
|
||||
"fluent-syntax",
|
||||
"fluent-syntax 0.10.3",
|
||||
"intl-memoizer",
|
||||
"intl_pluralrules",
|
||||
"ouroboros",
|
||||
"ouroboros 0.8.3",
|
||||
"rustc-hash",
|
||||
"smallvec",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-bundle"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acf044eeb4872d9dbf2667541fbf461f5965c57e343878ad0fb24b5793fa007"
|
||||
dependencies = [
|
||||
"fluent-langneg",
|
||||
"fluent-syntax 0.11.0",
|
||||
"intl-memoizer",
|
||||
"intl_pluralrules",
|
||||
"ouroboros 0.9.1",
|
||||
"rustc-hash",
|
||||
"smallvec",
|
||||
"unic-langid",
|
||||
@ -1221,6 +1336,15 @@ version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784f660373ea898f712a7e67b43f35bf79608d46112747c29767d087611d716b"
|
||||
|
||||
[[package]]
|
||||
name = "fluent-syntax"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0abed97648395c902868fee9026de96483933faa54ea3b40d652f7dfe61ca78"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-template-macros"
|
||||
version = "0.6.0"
|
||||
@ -1243,10 +1367,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fa6b2186b959236019d67fb839f036c83a799edd4389c505678ca1b8d41e9ed"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"fluent",
|
||||
"fluent-bundle",
|
||||
"fluent 0.14.4",
|
||||
"fluent-bundle 0.14.4",
|
||||
"fluent-langneg",
|
||||
"fluent-syntax",
|
||||
"fluent-syntax 0.10.3",
|
||||
"fluent-template-macros",
|
||||
"flume",
|
||||
"heck",
|
||||
@ -1262,14 +1386,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.10.3"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "859e0fa5d4a9b5f73671712ce3e400fa17133c9b9faac607ada8e469f0b6be6c"
|
||||
checksum = "fa9d66b91e902db43baefd8e40c8678ce29db2cf1d88ebd715174368d5fe70a9"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"nanorand",
|
||||
"pin-project 1.0.6",
|
||||
"pin-project 1.0.7",
|
||||
"spinning_top",
|
||||
]
|
||||
|
||||
@ -1478,6 +1602,15 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
@ -1485,8 +1618,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1558,6 +1693,42 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-events"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "088514ec8ef284891c762c88a66b639b3a730134714692ee31829765c5bc814f"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-file"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f9fecfe46b5dc3cc46f58e98ba580cc714f2c93860796d002eb3527a465ef49"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"gloo-events",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.2.7"
|
||||
@ -1684,9 +1855,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.3.6"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc35c995b9d93ec174cf9a27d425c7892722101e14993cd227fdb51d70cf9589"
|
||||
checksum = "4a1ce40d6fc9764887c2fdc7305c3dcc429ba11ff981c1509416afd5697e4437"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
@ -1725,7 +1896,7 @@ dependencies = [
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project 1.0.6",
|
||||
"pin-project 1.0.7",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@ -1748,9 +1919,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21"
|
||||
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
|
||||
dependencies = [
|
||||
"matches",
|
||||
"unicode-bidi",
|
||||
@ -1874,9 +2045,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.21"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2"
|
||||
checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@ -1923,9 +2094,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "lexical-core"
|
||||
version = "0.7.5"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374"
|
||||
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags",
|
||||
@ -2293,7 +2464,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"lazy_static",
|
||||
"percent-encoding",
|
||||
"pin-project 1.0.6",
|
||||
"pin-project 1.0.7",
|
||||
"rand 0.8.3",
|
||||
"thiserror",
|
||||
]
|
||||
@ -2326,7 +2497,17 @@ version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f6d5c203fe8d786d9d7bec8203cbbff3eb2cf8410c0d70cfd05b3d5f5d545da"
|
||||
dependencies = [
|
||||
"ouroboros_macro",
|
||||
"ouroboros_macro 0.8.3",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc53e78022445d2d37b673c5aaeed945a7aacaa4aa89c867c1f28b2e6778e67d"
|
||||
dependencies = [
|
||||
"ouroboros_macro 0.9.1",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
@ -2343,6 +2524,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros_macro"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee93af29e97048340c10f5c1a1d88c754f48337a41fbd4fb8e1e20ce41c76936"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro-error",
|
||||
"proc-macro2 1.0.26",
|
||||
"quote 1.0.9",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.1"
|
||||
@ -2449,11 +2643,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.0.6"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc174859768806e91ae575187ada95c91a29e96a98dc5d2cd9a1fed039501ba6"
|
||||
checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4"
|
||||
dependencies = [
|
||||
"pin-project-internal 1.0.6",
|
||||
"pin-project-internal 1.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2469,9 +2663,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.0.6"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a490329918e856ed1b083f244e3bfe2d8c4f336407e4ea9e1a9f479ff09049e5"
|
||||
checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.26",
|
||||
"quote 1.0.9",
|
||||
@ -2589,6 +2783,7 @@ dependencies = [
|
||||
name = "pslink"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-identity",
|
||||
"actix-rt",
|
||||
"actix-server",
|
||||
@ -2597,9 +2792,11 @@ dependencies = [
|
||||
"actix-web-static-files",
|
||||
"anyhow",
|
||||
"argonautica",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dotenv",
|
||||
"enum-map",
|
||||
"fluent-langneg",
|
||||
"fluent-templates",
|
||||
"image",
|
||||
@ -2610,6 +2807,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"rpassword",
|
||||
"serde",
|
||||
"shared",
|
||||
"sqlx",
|
||||
"tempdir",
|
||||
"tera",
|
||||
@ -2633,6 +2831,18 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"getopts",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "0.12.0"
|
||||
@ -2700,7 +2910,7 @@ dependencies = [
|
||||
"rand_isaac",
|
||||
"rand_jitter",
|
||||
"rand_os",
|
||||
"rand_pcg",
|
||||
"rand_pcg 0.1.2",
|
||||
"rand_xorshift",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
@ -2716,6 +2926,7 @@ dependencies = [
|
||||
"rand_chacha 0.2.2",
|
||||
"rand_core 0.5.1",
|
||||
"rand_hc 0.2.0",
|
||||
"rand_pcg 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2864,6 +3075,15 @@ dependencies = [
|
||||
"rand_core 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_pcg"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
|
||||
dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xorshift"
|
||||
version = "0.1.1"
|
||||
@ -2909,18 +3129,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
|
||||
checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.4.5"
|
||||
version = "1.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19"
|
||||
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -3135,6 +3355,32 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "seed"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b599be9cc57456f4b7fc99b8abfb154d4819f7b6c147e80be5580663dad4536"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"cookie",
|
||||
"dbg",
|
||||
"enclose",
|
||||
"futures 0.3.14",
|
||||
"gloo-file",
|
||||
"gloo-timers",
|
||||
"indexmap",
|
||||
"js-sys",
|
||||
"pulldown-cmark",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
"version_check 0.9.3",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "0.9.0"
|
||||
@ -3266,6 +3512,15 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"enum-map",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.3.0"
|
||||
@ -3277,9 +3532,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
|
||||
checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
|
||||
|
||||
[[package]]
|
||||
name = "slog"
|
||||
@ -3539,6 +3794,24 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2 1.0.26",
|
||||
"quote 1.0.9",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.0"
|
||||
@ -3547,9 +3820,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.69"
|
||||
version = "1.0.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb"
|
||||
checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.26",
|
||||
"quote 1.0.9",
|
||||
@ -3600,9 +3873,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tera"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cb278a72e426f291faf182cb0e0cb7d20241e8e9881046724ac874a83c62346"
|
||||
checksum = "b64b021b8d3ab1f59ceae9e6cd1c26c8e7ce0322a9ebfff6c0e22b3b66938935"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
@ -3920,7 +4193,7 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
|
||||
dependencies = [
|
||||
"pin-project 1.0.6",
|
||||
"pin-project 1.0.7",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@ -4234,10 +4507,42 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.11"
|
||||
name = "v_escape"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
|
||||
checksum = "f3e0ab5fab1db278a9413d2ea794cb66f471f898c5b020c3c394f6447625d9d4"
|
||||
dependencies = [
|
||||
"buf-min",
|
||||
"v_escape_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "v_escape_derive"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c860ad1273f4eee7006cee05db20c9e60e5d24cba024a32e1094aa8e574f3668"
|
||||
dependencies = [
|
||||
"nom 4.2.3",
|
||||
"proc-macro2 1.0.26",
|
||||
"quote 1.0.9",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "v_htmlescape"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f9a8af610ad6f7fc9989c9d2590d9764bc61f294884e9ee93baa58795174572"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"v_escape",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
@ -4389,9 +4694,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a32b378380f4e9869b22f0b5177c68a5519f03b3454fde0b291455ddbae266c"
|
||||
checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e"
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
|
78
Cargo.toml
78
Cargo.toml
@ -1,73 +1,5 @@
|
||||
[package]
|
||||
authors = ["Dietrich <dietrich@teilgedanken.de>"]
|
||||
build = "build.rs"
|
||||
categories = ["web-programming", "network-programming", "web-programming::http-server", "command-line-utilities"]
|
||||
description = "A simple webservice that allows registered users to create short links including qr-codes.\nAnyone can visit the shortened links. This is an ideal setup for small busines or for publishing papers."
|
||||
edition = "2018"
|
||||
keywords = ["url", "link", "webpage", "actix", "web"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
name = "pslink"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/enaut/pslink/"
|
||||
version = "0.3.1"
|
||||
[build-dependencies]
|
||||
actix-web-static-files = "3.0"
|
||||
|
||||
[dependencies]
|
||||
actix-identity = "0.3"
|
||||
actix-rt = "1.1"
|
||||
actix-slog = "0.2"
|
||||
actix-web = "3"
|
||||
actix-web-static-files = "3.0"
|
||||
anyhow = "1.0"
|
||||
argonautica = "0.2"
|
||||
clap = "2.33"
|
||||
dotenv = "0.15.0"
|
||||
fluent-langneg = "0.13"
|
||||
image = "0.23"
|
||||
opentelemetry = "0.13"
|
||||
opentelemetry-jaeger = "0.12"
|
||||
qrcode = "0.12"
|
||||
rand = "0.8"
|
||||
rpassword = "5.0"
|
||||
serde = "1.0"
|
||||
tera = "1.6"
|
||||
thiserror = "1.0"
|
||||
tracing-actix-web = "0.2.1"
|
||||
tracing-bunyan-formatter = "0.2.0"
|
||||
tracing-opentelemetry = "0.12"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.fluent-templates]
|
||||
features = ["tera"]
|
||||
version = "0.6"
|
||||
|
||||
[dependencies.sqlx]
|
||||
features = ["sqlite", "macros", "runtime-actix-rustls", "chrono", "migrate", "offline"]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.tracing]
|
||||
features = ["log"]
|
||||
version = "0.1"
|
||||
|
||||
[dependencies.tracing-subscriber]
|
||||
features = ["registry", "env-filter"]
|
||||
version = "0.2.17"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-server = "1.0.4"
|
||||
tempdir = "0.3"
|
||||
test_bin = "0.3"
|
||||
tokio = "0.2.25"
|
||||
|
||||
[dev-dependencies.reqwest]
|
||||
features = ["cookies"]
|
||||
version = "0.10.10"
|
||||
|
||||
[profile]
|
||||
[profile.release]
|
||||
lto = true
|
||||
#codegen-units = 1
|
||||
[workspace]
|
||||
members = [
|
||||
"pslink",
|
||||
"app",
|
||||
]
|
85
Makefile.toml
Normal file
85
Makefile.toml
Normal file
@ -0,0 +1,85 @@
|
||||
[config]
|
||||
default_to_workspace = false
|
||||
|
||||
# ---- BUILD ----
|
||||
|
||||
[tasks.build]
|
||||
description = "Build client and server"
|
||||
clear = true
|
||||
dependencies = ["build_client", "build_server"]
|
||||
|
||||
[tasks.build_release]
|
||||
extend = "build"
|
||||
description = "Build client and server in release mode"
|
||||
dependencies = ["build_client_release", "build_server_release"]
|
||||
|
||||
[tasks.build_client]
|
||||
description = "Build client"
|
||||
install_crate = { crate_name = "wasm-pack", binary = "wasm-pack", test_arg = "-V" }
|
||||
command = "wasm-pack"
|
||||
args = ["build", "app", "--target", "web", "--out-name", "package", "--dev"]
|
||||
|
||||
[tasks.build_client_release]
|
||||
extend = "build_client"
|
||||
description = "Build client in release mode"
|
||||
args = ["build", "app", "--target", "web", "--out-name", "package", "--release"]
|
||||
|
||||
[tasks.build_server]
|
||||
description = "Build server"
|
||||
command = "cargo"
|
||||
args = ["build", "--package", "pslink"]
|
||||
|
||||
[tasks.build_server_release]
|
||||
extend = "build_server"
|
||||
description = "Build server in release mode"
|
||||
args = ["build", "--package", "pslink", "--release"]
|
||||
|
||||
# ---- START ----
|
||||
|
||||
[tasks.start]
|
||||
description = "Build and start Actix server with client on port 8000"
|
||||
command = "cargo"
|
||||
args = ["run", "--package", "pslink", "--", "runserver"]
|
||||
dependencies = ["build"]
|
||||
|
||||
[tasks.start_release]
|
||||
extend = "start"
|
||||
description = "Build and start Actix server with client on port 8000 in release mode"
|
||||
args = ["run", "--package", "pslink", "--release", "--", "runserver"]
|
||||
dependencies = ["build_release"]
|
||||
|
||||
# ---- TEST ----
|
||||
|
||||
[tasks.test_firefox]
|
||||
description = "Test with wasm-pack in Firefox"
|
||||
command = "wasm-pack"
|
||||
args = ["test", "client", "--firefox", "--headless"]
|
||||
|
||||
# ---- LINT ----
|
||||
|
||||
[tasks.fmt]
|
||||
description = "Format with rustfmt"
|
||||
install_crate = { crate_name = "rustfmt-nightly", rustup_component_name = "rustfmt", binary = "rustfmt", test_arg = "--help" }
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all"]
|
||||
|
||||
[tasks.fmt_check]
|
||||
extend = "fmt"
|
||||
description = "Check format with rustfmt"
|
||||
args = ["fmt", "--all", "--", "--check"]
|
||||
|
||||
[tasks.clippy]
|
||||
description = "Lint with Clippy"
|
||||
clear = true
|
||||
install_crate = { rustup_component_name = "clippy", binary = "cargo-clippy", test_arg = "--help" }
|
||||
command = "cargo"
|
||||
args = ["clippy", "--all-features", "--",
|
||||
"--deny", "warnings",
|
||||
"--deny", "clippy::pedantic",
|
||||
"--deny", "clippy::nursery",
|
||||
"--allow", "clippy::wildcard_imports", # for `use seed::{prelude::*, *};`
|
||||
"--allow", "clippy::future_not_send", # JS/WASM is single threaded
|
||||
"--allow", "clippy::used_underscore_binding", # some libraries break this rule
|
||||
"--allow", "clippy::eval_order_dependence", # false positives
|
||||
"--allow", "clippy::vec_init_then_push", # Vec::new() + push are used in macros in shortcuts.rs
|
||||
]
|
30
app/Cargo.toml
Normal file
30
app/Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "app"
|
||||
authors = ["Dietrich <dietrich@teilgedanken.de>"]
|
||||
categories = ["web-programming", "network-programming", "web-programming::http-server", "command-line-utilities"]
|
||||
description = "A wasm frontend for pslink. The frontend communicates via json API."
|
||||
edition = "2018"
|
||||
keywords = ["url", "link", "webpage", "actix", "web"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/enaut/pslink/"
|
||||
version = "0.3.1"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
fluent = "0.15"
|
||||
seed = "0.8"
|
||||
serde = "1.0"
|
||||
strum_macros = "0.20"
|
||||
unic-langid = "0.9"
|
||||
strum = "0.20"
|
||||
enum-map = "1"
|
||||
|
||||
shared = { path = "../shared" }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 'z'
|
38
app/locales/de/main.ftl
Normal file
38
app/locales/de/main.ftl
Normal file
@ -0,0 +1,38 @@
|
||||
list-links = Link Liste
|
||||
add-link = Link hinzufügen
|
||||
invite-user = Benutzer einladen
|
||||
list-users = Liste der Benutzer
|
||||
welcome-user = Herzlich willkommen {$username}
|
||||
logout = Abmelden
|
||||
login = Login
|
||||
|
||||
not-found = Dieser Link existiert nicht, oder wurde gelöscht.
|
||||
|
||||
edit-link-headline = Zu editierender Link: {$linktitle}
|
||||
edit-link = Link Editieren
|
||||
create-link = Link Erstellen
|
||||
link-description = Beschreibung
|
||||
link-target = Link Ziel
|
||||
link-code = Link Code
|
||||
shortlink = Shortlink
|
||||
qr-code = QR-code
|
||||
search-placeholder = Filtern nach...
|
||||
|
||||
danger-zone = Achtung!
|
||||
danger-zone-text = Verändern Sie den Code von bereits veröffentlichten Links nicht. Sollte es dennoch geschehen werden veröffentlichte links unbenutzbar. Wird das Linkziel verändert, so zeigen auch die bereits veröffentlichten Links auf das neue Ziel.
|
||||
|
||||
save-edits = Speichere die Veränderungen
|
||||
delete-link = Diesen Link löschen
|
||||
|
||||
edit-user-headline = Benutzereinstellungen von: {$username}
|
||||
username = Benutzername
|
||||
email = Email
|
||||
password = Passwort
|
||||
password-placeholder = Leer lassen um das Passwort nicht zu ändern
|
||||
save-user = Benutzer speichern
|
||||
edit-user = Benutzer editieren
|
||||
make-user-admin = Zum Administrator befördern
|
||||
make-user-regular = Zurückstufen zum normalen Nutzer
|
||||
|
||||
userid = Benutzernummer
|
||||
statistics = Statistik
|
38
app/locales/en/main.ftl
Normal file
38
app/locales/en/main.ftl
Normal file
@ -0,0 +1,38 @@
|
||||
list-links = List of existing links
|
||||
add-link = Add a new link
|
||||
invite-user = Invite a new user
|
||||
list-users = List of existing users
|
||||
welcome-user = Welcome {$username}
|
||||
logout = Logout
|
||||
login = Login
|
||||
|
||||
not-found = This Link has not been found or has been deleted
|
||||
|
||||
edit-link-headline = Edit link: {$linktitle}
|
||||
edit-link = Edit link
|
||||
create-link = Create link
|
||||
link-description = Description
|
||||
link-target = Link target
|
||||
link-code = Link code
|
||||
shortlink = Shortlink
|
||||
search-placeholder = Filter according to...
|
||||
|
||||
danger-zone = Danger Zone!
|
||||
danger-zone-text = Do not change the code of links that are published. If you do so the published links will become invalid! If you change the target the published links will point to the new target.
|
||||
|
||||
save-edits = Save edits
|
||||
delete-link = Delete this link
|
||||
|
||||
user-headline = User Settings of: {$username}
|
||||
edit-user-headline = Change Settings of: {$username}
|
||||
username = Username
|
||||
email = Email
|
||||
password = Password
|
||||
password-placeholder = Leave this empty to keep the current password
|
||||
save-user = Save this user
|
||||
edit-user = Edit this user
|
||||
make-user-admin = Promote to admin
|
||||
make-user-regular = Demote to regular
|
||||
|
||||
userid = User ID
|
||||
statistics = Statistics
|
147
app/src/i18n.rs
Normal file
147
app/src/i18n.rs
Normal file
@ -0,0 +1,147 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fluent::{FluentArgs, FluentBundle, FluentResource};
|
||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
||||
// ------ I18n ------
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct I18n {
|
||||
lang: Lang,
|
||||
ftl_bundle: Arc<FluentBundle<FluentResource>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for I18n {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self.lang)
|
||||
}
|
||||
}
|
||||
|
||||
impl I18n {
|
||||
#[must_use]
|
||||
pub fn new(lang: Lang) -> Self {
|
||||
Self {
|
||||
lang,
|
||||
ftl_bundle: Arc::new(lang.create_ftl_bundle()),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn lang(&self) -> &Lang {
|
||||
&self.lang
|
||||
}
|
||||
|
||||
pub fn set_lang(&mut self, lang: Lang) -> &Self {
|
||||
self.lang = lang;
|
||||
self.ftl_bundle = Arc::new(lang.create_ftl_bundle());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn translate(&self, key: impl AsRef<str>, args: Option<&FluentArgs>) -> String {
|
||||
let msg = self
|
||||
.ftl_bundle
|
||||
.get_message(key.as_ref())
|
||||
.expect("get fluent message");
|
||||
|
||||
let pattern = msg.value().expect("get value for fluent message");
|
||||
|
||||
self.ftl_bundle
|
||||
.format_pattern(pattern, args, &mut vec![])
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// ------ Lang ------
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Debug, Copy, Clone, Display, EnumIter, EnumString, AsRefStr, Eq, PartialEq)]
|
||||
pub enum Lang {
|
||||
#[strum(serialize = "en-US")]
|
||||
EnUS,
|
||||
#[strum(serialize = "de-DE")]
|
||||
DeDE,
|
||||
}
|
||||
|
||||
impl Lang {
|
||||
#[must_use]
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::EnUS => "English (US)",
|
||||
Self::DeDE => "Deutsch (Deutschland)",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn ftl_messages(self) -> &'static str {
|
||||
macro_rules! include_ftl_messages {
|
||||
( $lang_id:literal ) => {
|
||||
include_str!(concat!("../locales/", $lang_id, "/main.ftl"))
|
||||
};
|
||||
}
|
||||
match self {
|
||||
Self::EnUS => include_ftl_messages!("en"),
|
||||
Self::DeDE => include_ftl_messages!("de"),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_language_identifier(self) -> LanguageIdentifier {
|
||||
self.as_ref()
|
||||
.parse()
|
||||
.expect("parse Lang to LanguageIdentifier")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn create_ftl_bundle(self) -> FluentBundle<FluentResource> {
|
||||
let ftl_resource =
|
||||
FluentResource::try_new(self.ftl_messages().to_owned()).expect("parse FTL messages");
|
||||
|
||||
let mut bundle = FluentBundle::new(vec![self.to_language_identifier()]);
|
||||
bundle.add_resource(ftl_resource).expect("add FTL resource");
|
||||
bundle
|
||||
}
|
||||
}
|
||||
|
||||
// ------ create_t ------
|
||||
|
||||
/// Convenience macro to improve readability of `view`s with many translations.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
///```rust,no_run
|
||||
/// fn view(model: &Model) -> Node<Msg> {
|
||||
/// let args_male_sg = fluent_args![
|
||||
/// "userName" => "Stephan",
|
||||
/// "userGender" => "male",
|
||||
/// ];
|
||||
///
|
||||
/// create_t!(model.i18n);
|
||||
/// div![
|
||||
/// p![t!("hello-world")],
|
||||
/// p![t!("hello-user", args_male_sg)],
|
||||
/// ]
|
||||
/// }
|
||||
///```
|
||||
#[macro_export]
|
||||
macro_rules! create_t {
|
||||
( $i18n:expr ) => {
|
||||
// This replaces $d with $ in the inner macro.
|
||||
seed::with_dollar_sign! {
|
||||
($d:tt) => {
|
||||
macro_rules! t {
|
||||
{ $d key:expr } => {
|
||||
{
|
||||
$i18n.translate($d key, None)
|
||||
}
|
||||
};
|
||||
{ $d key:expr, $d args:expr } => {
|
||||
{
|
||||
$i18n.translate($d key, Some(&$d args))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
177
app/src/lib.rs
Normal file
177
app/src/lib.rs
Normal file
@ -0,0 +1,177 @@
|
||||
pub mod i18n;
|
||||
pub mod navigation;
|
||||
pub mod pages;
|
||||
|
||||
use pages::list_links;
|
||||
use pages::list_users;
|
||||
use seed::{div, log, prelude::*, App, Url, C};
|
||||
|
||||
use crate::i18n::{I18n, Lang};
|
||||
|
||||
// ------ ------
|
||||
// Init
|
||||
// ------ ------
|
||||
|
||||
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
||||
orders.subscribe(Msg::UrlChanged);
|
||||
|
||||
log!(url);
|
||||
|
||||
let lang = I18n::new(Lang::DeDE);
|
||||
|
||||
Model {
|
||||
index: 0,
|
||||
base_url: Url::new().add_path_part("app"),
|
||||
page: Page::init(url, orders, lang.clone()),
|
||||
i18n: lang,
|
||||
}
|
||||
}
|
||||
|
||||
// ------ ------
|
||||
// Model
|
||||
// ------ ------
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Model {
|
||||
index: usize,
|
||||
base_url: Url,
|
||||
page: Page,
|
||||
i18n: i18n::I18n,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Page {
|
||||
Home(pages::list_links::Model),
|
||||
ListUsers(pages::list_users::Model),
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Self {
|
||||
url.next_path_part();
|
||||
let result = match url.remaining_path_parts().as_slice() {
|
||||
[] | ["list_links"] => Self::Home(pages::list_links::init(
|
||||
url,
|
||||
&mut orders.proxy(Msg::ListLinksMsg),
|
||||
i18n,
|
||||
)),
|
||||
["list_users"] => Self::ListUsers(pages::list_users::init(
|
||||
url,
|
||||
&mut orders.proxy(Msg::ListUsersMsg),
|
||||
i18n,
|
||||
)),
|
||||
_other => Self::NotFound,
|
||||
};
|
||||
|
||||
log!("Page initialized");
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// ------ ------
|
||||
// Update
|
||||
// ------ ------
|
||||
#[allow(renamed_and_removed_lints, pub_enum_variant_names)]
|
||||
#[derive(Clone)]
|
||||
pub enum Msg {
|
||||
UrlChanged(subs::UrlChanged),
|
||||
ListLinksMsg(list_links::Msg),
|
||||
ListUsersMsg(list_users::Msg),
|
||||
NoMessage,
|
||||
}
|
||||
|
||||
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::UrlChanged(url) => {
|
||||
log!("Url changed!");
|
||||
|
||||
model.page = Page::init(url.0, orders, model.i18n.clone());
|
||||
}
|
||||
Msg::ListLinksMsg(msg) => {
|
||||
if let Page::Home(model) = &mut model.page {
|
||||
list_links::update(msg, model, &mut orders.proxy(Msg::ListLinksMsg))
|
||||
}
|
||||
}
|
||||
Msg::ListUsersMsg(msg) => {
|
||||
if let Page::ListUsers(model) = &mut model.page {
|
||||
list_users::update(msg, model, &mut orders.proxy(Msg::ListUsersMsg))
|
||||
}
|
||||
}
|
||||
Msg::NoMessage => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Urls<'a> {
|
||||
base_url: std::borrow::Cow<'a, Url>,
|
||||
}
|
||||
|
||||
impl<'a> Urls<'a> {
|
||||
/// Create a new `Urls` instance.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// Urls::new(base_url).home()
|
||||
/// ```
|
||||
pub fn new(base_url: impl Into<std::borrow::Cow<'a, Url>>) -> Self {
|
||||
Self {
|
||||
base_url: base_url.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return base `Url`. If `base_url` isn't owned, it will be cloned.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// pub fn admin_urls(self) -> page::admin::Urls<'a> {
|
||||
/// page::admin::Urls::new(self.base_url().add_path_part(ADMIN))
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn base_url(self) -> Url {
|
||||
self.base_url.into_owned()
|
||||
}
|
||||
#[must_use]
|
||||
pub fn home(self) -> Url {
|
||||
self.base_url()
|
||||
}
|
||||
#[must_use]
|
||||
pub fn list_links(self) -> Url {
|
||||
self.base_url().add_path_part("list_links")
|
||||
}
|
||||
#[must_use]
|
||||
pub fn list_users(self) -> Url {
|
||||
self.base_url().add_path_part("list_users")
|
||||
}
|
||||
}
|
||||
|
||||
// ------ ------
|
||||
// View
|
||||
// ------ ------
|
||||
|
||||
fn view(model: &Model) -> Node<Msg> {
|
||||
div![
|
||||
C!["page"],
|
||||
navigation::navigation(&model.i18n, &model.base_url,),
|
||||
view_content(&model.page, &model.base_url),
|
||||
]
|
||||
}
|
||||
fn view_content(page: &Page, url: &Url) -> Node<Msg> {
|
||||
div![
|
||||
C!["container"],
|
||||
match page {
|
||||
Page::Home(model) => pages::list_links::view(model).map_msg(Msg::ListLinksMsg),
|
||||
Page::ListUsers(model) => pages::list_users::view(model).map_msg(Msg::ListUsersMsg),
|
||||
Page::NotFound => div![div![url.to_string()], "Page not found!"],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// ------ ------
|
||||
// Start
|
||||
// ------ ------
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main() {
|
||||
App::start("app", init, update, view);
|
||||
}
|
46
app/src/navigation.rs
Normal file
46
app/src/navigation.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use fluent::fluent_args;
|
||||
use seed::{a, attrs, div, li, nav, ol, prelude::*, Url};
|
||||
|
||||
use crate::{i18n::I18n, Msg};
|
||||
#[must_use]
|
||||
pub fn navigation(i18n: &I18n, base_url: &Url) -> Node<Msg> {
|
||||
let username = fluent_args![ "username" => "enaut"];
|
||||
macro_rules! t {
|
||||
{ $key:expr } => {
|
||||
{
|
||||
i18n.translate($key, None)
|
||||
}
|
||||
};
|
||||
{ $key:expr, $args:expr } => {
|
||||
{
|
||||
i18n.translate($key, Some(&$args))
|
||||
}
|
||||
};
|
||||
}
|
||||
nav![
|
||||
ol![
|
||||
li![a![
|
||||
attrs! {At::Href => crate::Urls::new(base_url).list_links()},
|
||||
t!("list-links"),
|
||||
],],
|
||||
li![a![ev(Ev::Click, |_| Msg::NoMessage), t!("add-link"),],],
|
||||
li![a![
|
||||
attrs! {At::Href => "#"},
|
||||
ev(Ev::Click, |_| Msg::NoMessage),
|
||||
t!("invite-user"),
|
||||
],],
|
||||
li![a![
|
||||
attrs! {At::Href => crate::Urls::new(base_url).list_users()},
|
||||
t!("list-users"),
|
||||
],],
|
||||
],
|
||||
ol![
|
||||
li![div![t!("welcome-user", username)]],
|
||||
li![a![
|
||||
attrs! {At::Href => "#"},
|
||||
ev(Ev::Click, |_| Msg::NoMessage),
|
||||
t!("logout"),
|
||||
]]
|
||||
]
|
||||
]
|
||||
}
|
6
app/src/pages/home.rs
Normal file
6
app/src/pages/home.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use seed::{div, prelude::*};
|
||||
|
||||
#[must_use]
|
||||
pub fn view<Ms>() -> Node<Ms> {
|
||||
div!["List Links"]
|
||||
}
|
247
app/src/pages/list_links.rs
Normal file
247
app/src/pages/list_links.rs
Normal file
@ -0,0 +1,247 @@
|
||||
use enum_map::EnumMap;
|
||||
use seed::{a, attrs, button, h1, input, log, prelude::*, section, table, td, th, tr, Url, C};
|
||||
|
||||
use shared::{
|
||||
apirequests::general::Ordering,
|
||||
apirequests::{
|
||||
general::Operation,
|
||||
links::{LinkOverviewColumns, LinkRequestForm},
|
||||
},
|
||||
datatypes::FullLink,
|
||||
};
|
||||
|
||||
use crate::i18n::I18n;
|
||||
|
||||
pub fn init(_: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
|
||||
orders.send_msg(Msg::Fetch);
|
||||
|
||||
Model {
|
||||
links: Vec::new(),
|
||||
i18n,
|
||||
formconfig: LinkRequestForm::default(),
|
||||
inputs: EnumMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Model {
|
||||
links: Vec<FullLink>,
|
||||
i18n: I18n,
|
||||
formconfig: LinkRequestForm,
|
||||
inputs: EnumMap<LinkOverviewColumns, FilterInput>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct FilterInput {
|
||||
filter_input: ElRef<web_sys::HtmlInputElement>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Msg {
|
||||
Fetch,
|
||||
OrderBy(LinkOverviewColumns),
|
||||
Received(Vec<FullLink>),
|
||||
CodeFilterChanged(String),
|
||||
DescriptionFilterChanged(String),
|
||||
TargetFilterChanged(String),
|
||||
AuthorFilterChanged(String),
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
/// Sould only panic on bugs.
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::Fetch => {
|
||||
orders.skip(); // No need to rerender
|
||||
let data = model.formconfig.clone(); // complicated way to move into the closure
|
||||
orders.perform_cmd(async {
|
||||
let data = data;
|
||||
let response = fetch(
|
||||
Request::new("/admin/json/list_links/")
|
||||
.method(Method::Post)
|
||||
.json(&data)
|
||||
.expect("serialization failed"),
|
||||
)
|
||||
.await
|
||||
.expect("HTTP request failed");
|
||||
|
||||
let user: Vec<FullLink> = response
|
||||
.check_status() // ensure we've got 2xx status
|
||||
.expect("status check failed")
|
||||
.json()
|
||||
.await
|
||||
.expect("deserialization failed");
|
||||
|
||||
Msg::Received(user)
|
||||
});
|
||||
}
|
||||
Msg::OrderBy(column) => {
|
||||
model.formconfig.order = model.formconfig.order.as_ref().map_or_else(
|
||||
|| {
|
||||
Some(Operation {
|
||||
column: column.clone(),
|
||||
value: Ordering::Ascending,
|
||||
})
|
||||
},
|
||||
|order| {
|
||||
Some(Operation {
|
||||
column: column.clone(),
|
||||
value: if order.column == column && order.value == Ordering::Ascending {
|
||||
Ordering::Descending
|
||||
} else {
|
||||
Ordering::Ascending
|
||||
},
|
||||
})
|
||||
},
|
||||
);
|
||||
orders.send_msg(Msg::Fetch);
|
||||
|
||||
model.links.sort_by(match column {
|
||||
LinkOverviewColumns::Code => {
|
||||
|o: &FullLink, t: &FullLink| o.link.code.cmp(&t.link.code)
|
||||
}
|
||||
LinkOverviewColumns::Description => {
|
||||
|o: &FullLink, t: &FullLink| o.link.title.cmp(&t.link.title)
|
||||
}
|
||||
LinkOverviewColumns::Target => {
|
||||
|o: &FullLink, t: &FullLink| o.link.target.cmp(&t.link.target)
|
||||
}
|
||||
LinkOverviewColumns::Author => {
|
||||
|o: &FullLink, t: &FullLink| o.user.username.cmp(&t.user.username)
|
||||
}
|
||||
LinkOverviewColumns::Statistics => {
|
||||
|o: &FullLink, t: &FullLink| o.clicks.number.cmp(&t.clicks.number)
|
||||
}
|
||||
})
|
||||
}
|
||||
Msg::Received(response) => {
|
||||
model.links = response;
|
||||
}
|
||||
Msg::CodeFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
model.formconfig.filter[LinkOverviewColumns::Code].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
Msg::DescriptionFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
model.formconfig.filter[LinkOverviewColumns::Description].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
Msg::TargetFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
model.formconfig.filter[LinkOverviewColumns::Target].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
Msg::AuthorFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
model.formconfig.filter[LinkOverviewColumns::Author].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// # Panics
|
||||
/// Sould only panic on bugs.
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
macro_rules! t {
|
||||
{ $key:expr } => {
|
||||
{
|
||||
model.i18n.translate($key, None)
|
||||
}
|
||||
};
|
||||
{ $key:expr, $args:expr } => {
|
||||
{
|
||||
model.i18n.translate($key, Some(&$args))
|
||||
}
|
||||
};
|
||||
}
|
||||
section![
|
||||
h1!("List Links Page from list_links"),
|
||||
table![
|
||||
tr![
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Code)),
|
||||
t!("link-code")
|
||||
],
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(
|
||||
LinkOverviewColumns::Description
|
||||
)),
|
||||
t!("link-description")
|
||||
],
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Target)),
|
||||
t!("link-target")
|
||||
],
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Author)),
|
||||
t!("username")
|
||||
],
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Statistics)),
|
||||
t!("statistics")
|
||||
]
|
||||
],
|
||||
tr![
|
||||
C!["filters"],
|
||||
td![input![
|
||||
attrs! {
|
||||
At::Value => &model.formconfig.filter[LinkOverviewColumns::Code].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::CodeFilterChanged),
|
||||
el_ref(&model.inputs[LinkOverviewColumns::Code].filter_input),
|
||||
]],
|
||||
td![input![
|
||||
attrs! {At::Value =>
|
||||
&model
|
||||
.formconfig.filter[LinkOverviewColumns::Description].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::DescriptionFilterChanged),
|
||||
el_ref(&model.inputs[LinkOverviewColumns::Description].filter_input),
|
||||
]],
|
||||
td![input![
|
||||
attrs! {At::Value =>
|
||||
&model
|
||||
.formconfig.filter[LinkOverviewColumns::Target].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::TargetFilterChanged),
|
||||
el_ref(&model.inputs[LinkOverviewColumns::Target].filter_input),
|
||||
]],
|
||||
td![input![
|
||||
attrs! {At::Value =>
|
||||
&model
|
||||
.formconfig.filter[LinkOverviewColumns::Author].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::AuthorFilterChanged),
|
||||
el_ref(&model.inputs[LinkOverviewColumns::Author].filter_input),
|
||||
]],
|
||||
td![]
|
||||
],
|
||||
model.links.iter().map(view_link)
|
||||
],
|
||||
button![ev(Ev::Click, |_| Msg::Fetch), "Fetch links"]
|
||||
]
|
||||
}
|
||||
|
||||
fn view_link(l: &FullLink) -> Node<Msg> {
|
||||
tr![
|
||||
td![&l.link.code],
|
||||
td![&l.link.title],
|
||||
td![a![attrs![At::Href => &l.link.target], &l.link.target]],
|
||||
td![&l.user.username],
|
||||
td![&l.clicks.number]
|
||||
]
|
||||
}
|
200
app/src/pages/list_users.rs
Normal file
200
app/src/pages/list_users.rs
Normal file
@ -0,0 +1,200 @@
|
||||
use enum_map::EnumMap;
|
||||
use seed::{attrs, button, h1, input, log, prelude::*, section, table, td, th, tr, Url, C};
|
||||
use shared::{
|
||||
apirequests::general::{Operation, Ordering},
|
||||
apirequests::users::{UserOverviewColumns, UserRequestForm},
|
||||
datatypes::User,
|
||||
};
|
||||
|
||||
use crate::i18n::I18n;
|
||||
#[must_use]
|
||||
pub fn init(_: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
|
||||
orders.send_msg(Msg::Fetch);
|
||||
Model {
|
||||
users: Vec::new(),
|
||||
i18n,
|
||||
formconfig: UserRequestForm::default(),
|
||||
inputs: EnumMap::default(),
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct Model {
|
||||
users: Vec<User>,
|
||||
i18n: I18n,
|
||||
formconfig: UserRequestForm,
|
||||
inputs: EnumMap<UserOverviewColumns, FilterInput>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct FilterInput {
|
||||
filter_input: ElRef<web_sys::HtmlInputElement>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Msg {
|
||||
Fetch,
|
||||
OrderBy(UserOverviewColumns),
|
||||
Received(Vec<User>),
|
||||
IdFilterChanged(String),
|
||||
EmailFilterChanged(String),
|
||||
UsernameFilterChanged(String),
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
/// Sould only panic on bugs.
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::Fetch => {
|
||||
orders.skip(); // No need to rerender
|
||||
let data = model.formconfig.clone(); // complicated way to move into the closure
|
||||
orders.perform_cmd(async {
|
||||
let data = data;
|
||||
let response = fetch(
|
||||
Request::new("/admin/json/list_users/")
|
||||
.method(Method::Post)
|
||||
.json(&data)
|
||||
.expect("serialization failed"),
|
||||
)
|
||||
.await
|
||||
.expect("HTTP request failed");
|
||||
|
||||
let users: Vec<User> = response
|
||||
.check_status() // ensure we've got 2xx status
|
||||
.expect("status check failed")
|
||||
.json()
|
||||
.await
|
||||
.expect("deserialization failed");
|
||||
|
||||
Msg::Received(users)
|
||||
});
|
||||
}
|
||||
Msg::OrderBy(column) => {
|
||||
model.formconfig.order = model.formconfig.order.as_ref().map_or_else(
|
||||
|| {
|
||||
Some(Operation {
|
||||
column: column.clone(),
|
||||
value: Ordering::Ascending,
|
||||
})
|
||||
},
|
||||
|order| {
|
||||
Some(Operation {
|
||||
column: column.clone(),
|
||||
value: if order.column == column && order.value == Ordering::Ascending {
|
||||
Ordering::Descending
|
||||
} else {
|
||||
Ordering::Ascending
|
||||
},
|
||||
})
|
||||
},
|
||||
);
|
||||
orders.send_msg(Msg::Fetch);
|
||||
|
||||
model.users.sort_by(match column {
|
||||
UserOverviewColumns::Id => |o: &User, t: &User| o.id.cmp(&t.id),
|
||||
UserOverviewColumns::Username => |o: &User, t: &User| o.username.cmp(&t.username),
|
||||
UserOverviewColumns::Email => |o: &User, t: &User| o.email.cmp(&t.email),
|
||||
})
|
||||
}
|
||||
Msg::Received(response) => {
|
||||
model.users = response;
|
||||
}
|
||||
Msg::IdFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
let sanit = s.chars().filter(|x| x.is_numeric()).collect();
|
||||
model.formconfig.filter[UserOverviewColumns::Id].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
Msg::UsernameFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
model.formconfig.filter[UserOverviewColumns::Username].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
Msg::EmailFilterChanged(s) => {
|
||||
log!("Filter is: ", &s);
|
||||
// FIXME: Sanitazion does not work for @
|
||||
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
model.formconfig.filter[UserOverviewColumns::Email].sieve = sanit;
|
||||
orders.send_msg(Msg::Fetch);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[must_use]
|
||||
/// # Panics
|
||||
/// Sould only panic on bugs.
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
macro_rules! t {
|
||||
{ $key:expr } => {
|
||||
{
|
||||
model.i18n.translate($key, None)
|
||||
}
|
||||
};
|
||||
{ $key:expr, $args:expr } => {
|
||||
{
|
||||
model.i18n.translate($key, Some(&$args))
|
||||
}
|
||||
};
|
||||
}
|
||||
section![
|
||||
h1!("List Users Page from list_users"),
|
||||
table![
|
||||
tr![
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(UserOverviewColumns::Id)),
|
||||
t!("userid")
|
||||
],
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(UserOverviewColumns::Email)),
|
||||
t!("email")
|
||||
],
|
||||
th![
|
||||
ev(Ev::Click, |_| Msg::OrderBy(UserOverviewColumns::Username)),
|
||||
t!("username")
|
||||
],
|
||||
],
|
||||
tr![
|
||||
C!["filters"],
|
||||
td![input![
|
||||
attrs! {
|
||||
At::Value => &model.formconfig.filter[UserOverviewColumns::Id].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::IdFilterChanged),
|
||||
el_ref(&model.inputs[UserOverviewColumns::Id].filter_input),
|
||||
]],
|
||||
td![input![
|
||||
attrs! {At::Value =>
|
||||
&model
|
||||
.formconfig.filter[UserOverviewColumns::Email].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::EmailFilterChanged),
|
||||
el_ref(&model.inputs[UserOverviewColumns::Email].filter_input),
|
||||
]],
|
||||
td![input![
|
||||
attrs! {At::Value =>
|
||||
&model
|
||||
.formconfig.filter[UserOverviewColumns::Username].sieve,
|
||||
At::Type => "search",
|
||||
At::Placeholder => t!("search-placeholder")
|
||||
},
|
||||
input_ev(Ev::Input, Msg::UsernameFilterChanged),
|
||||
el_ref(&model.inputs[UserOverviewColumns::Username].filter_input),
|
||||
]],
|
||||
],
|
||||
model.users.iter().map(view_user)
|
||||
],
|
||||
button![ev(Ev::Click, |_| Msg::Fetch), "Refresh"]
|
||||
]
|
||||
}
|
||||
|
||||
fn view_user(l: &User) -> Node<Msg> {
|
||||
tr![
|
||||
td![&l.id],
|
||||
td![&l.email],
|
||||
//td![a![attrs![At::Href => &l.link.target], &l.link.target]],
|
||||
td![&l.username],
|
||||
]
|
||||
}
|
3
app/src/pages/mod.rs
Normal file
3
app/src/pages/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod home;
|
||||
pub mod list_links;
|
||||
pub mod list_users;
|
78
pslink/Cargo.toml
Normal file
78
pslink/Cargo.toml
Normal file
@ -0,0 +1,78 @@
|
||||
[package]
|
||||
authors = ["Dietrich <dietrich@teilgedanken.de>"]
|
||||
build = "build.rs"
|
||||
categories = ["web-programming", "network-programming", "web-programming::http-server", "command-line-utilities"]
|
||||
description = "A simple webservice that allows registered users to create short links including qr-codes.\nAnyone can visit the shortened links. This is an ideal setup for small busines or for publishing papers."
|
||||
edition = "2018"
|
||||
keywords = ["url", "link", "webpage", "actix", "web"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
name = "pslink"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/enaut/pslink/"
|
||||
version = "0.3.1"
|
||||
[build-dependencies]
|
||||
actix-web-static-files = "3.0"
|
||||
|
||||
[dependencies]
|
||||
actix-identity = "0.3"
|
||||
actix-rt = "1.1"
|
||||
actix-slog = "0.2"
|
||||
actix-web = "3"
|
||||
actix-web-static-files = "3.0"
|
||||
actix-files = "0.5.0"
|
||||
anyhow = "1.0"
|
||||
argonautica = "0.2"
|
||||
clap = "2.33"
|
||||
dotenv = "0.15.0"
|
||||
fluent-langneg = "0.13"
|
||||
image = "0.23"
|
||||
opentelemetry = "0.13"
|
||||
opentelemetry-jaeger = "0.12"
|
||||
qrcode = "0.12"
|
||||
rand = "0.8"
|
||||
rpassword = "5.0"
|
||||
serde = "1.0"
|
||||
tera = "1.6"
|
||||
thiserror = "1.0"
|
||||
tracing-actix-web = "0.2.1"
|
||||
tracing-bunyan-formatter = "0.2.0"
|
||||
tracing-opentelemetry = "0.12"
|
||||
async-trait = "0.1"
|
||||
enum-map = {version="1", features = ["serde"]}
|
||||
|
||||
shared = { path = "../shared" }
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.fluent-templates]
|
||||
features = ["tera"]
|
||||
version = "0.6"
|
||||
|
||||
[dependencies.sqlx]
|
||||
features = ["sqlite", "macros", "runtime-actix-rustls", "chrono", "migrate", "offline"]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.tracing]
|
||||
features = ["log"]
|
||||
version = "0.1"
|
||||
|
||||
[dependencies.tracing-subscriber]
|
||||
features = ["registry", "env-filter"]
|
||||
version = "0.2.17"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-server = "1.0.4"
|
||||
tempdir = "0.3"
|
||||
test_bin = "0.3"
|
||||
tokio = "0.2.25"
|
||||
|
||||
[dev-dependencies.reqwest]
|
||||
features = ["cookies"]
|
||||
version = "0.10.10"
|
||||
|
||||
[profile]
|
||||
[profile.release]
|
||||
lto = true
|
||||
#codegen-units = 1
|
@ -1,5 +1,5 @@
|
||||
use actix_web_static_files::resource_dir;
|
||||
|
||||
fn main() {
|
||||
resource_dir("./static").build().unwrap();
|
||||
resource_dir("./static/").build().unwrap();
|
||||
}
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
0
pslink/migrations/.gitkeep
Normal file
0
pslink/migrations/.gitkeep
Normal file
@ -3,6 +3,7 @@ use clap::{
|
||||
ArgMatches, SubCommand,
|
||||
};
|
||||
use dotenv::dotenv;
|
||||
use shared::datatypes::User;
|
||||
use sqlx::{migrate::Migrator, Pool, Sqlite};
|
||||
use std::{
|
||||
fs::File,
|
||||
@ -10,7 +11,10 @@ use std::{
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use pslink::{models::NewUser, models::User, ServerConfig, ServerError};
|
||||
use pslink::{
|
||||
models::{NewUser, UserDbOperations},
|
||||
ServerConfig, ServerError,
|
||||
};
|
||||
|
||||
use tracing::{error, info, trace, warn};
|
||||
|
||||
@ -308,7 +312,12 @@ async fn create_admin(config: &ServerConfig) -> Result<(), ServerError> {
|
||||
&new_username, &new_email
|
||||
);
|
||||
|
||||
let new_admin = NewUser::new(new_username.clone(), new_email.clone(), &password, config)?;
|
||||
let new_admin = NewUser::new(
|
||||
new_username.clone(),
|
||||
new_email.clone(),
|
||||
&password,
|
||||
&config.secret,
|
||||
)?;
|
||||
|
||||
new_admin.insert_user(config).await?;
|
||||
let created_user = User::get_user_by_name(&new_username, config).await?;
|
@ -5,6 +5,7 @@ pub mod models;
|
||||
pub mod queries;
|
||||
mod views;
|
||||
|
||||
use actix_files::Files;
|
||||
use actix_identity::{CookieIdentityPolicy, IdentityService};
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
@ -366,10 +367,20 @@ pub async fn webservice(
|
||||
web::scope("/download")
|
||||
.route("/png/{redirect_id}", web::get().to(views::download_png)),
|
||||
)
|
||||
.service(
|
||||
web::scope("/json")
|
||||
.route("/list_links/", web::post().to(views::index_json))
|
||||
.route("/list_users/", web::post().to(views::index_users_json)),
|
||||
)
|
||||
// login to the admin area
|
||||
.route("/login/", web::get().to(views::login))
|
||||
.route("/login/", web::post().to(views::process_login)),
|
||||
)
|
||||
.service(
|
||||
web::scope("/app")
|
||||
.service(Files::new("/pkg", "./app/pkg"))
|
||||
.default_service(web::get().to(views::wasm_app)),
|
||||
)
|
||||
// redirect to the url hidden behind the code
|
||||
.route("/{redirect_id}", web::get().to(views::redirect))
|
||||
})
|
@ -1,24 +1,30 @@
|
||||
use crate::{forms::LinkForm, ServerConfig, ServerError};
|
||||
use crate::{forms::LinkForm, Secret, ServerConfig, ServerError};
|
||||
|
||||
use argonautica::Hasher;
|
||||
use async_trait::async_trait;
|
||||
use dotenv::dotenv;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(PartialEq, Serialize, Clone, Debug)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub role: i64,
|
||||
pub language: String,
|
||||
use shared::datatypes::{Count, Link, User};
|
||||
|
||||
#[async_trait]
|
||||
pub trait UserDbOperations<T> {
|
||||
async fn get_user(id: i64, server_config: &ServerConfig) -> Result<T, ServerError>;
|
||||
async fn get_user_by_name(name: &str, server_config: &ServerConfig) -> Result<T, ServerError>;
|
||||
async fn get_all_users(server_config: &ServerConfig) -> Result<Vec<T>, ServerError>;
|
||||
async fn update_user(&self, server_config: &ServerConfig) -> Result<(), ServerError>;
|
||||
async fn toggle_admin(self, server_config: &ServerConfig) -> Result<(), ServerError>;
|
||||
async fn set_language(
|
||||
self,
|
||||
server_config: &ServerConfig,
|
||||
new_language: &str,
|
||||
) -> Result<(), ServerError>;
|
||||
async fn count_admins(server_config: &ServerConfig) -> Result<Count, ServerError>;
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub(crate) async fn get_user(
|
||||
id: i64,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<Self, ServerError> {
|
||||
#[async_trait]
|
||||
impl UserDbOperations<User> for User {
|
||||
async fn get_user(id: i64, server_config: &ServerConfig) -> Result<Self, ServerError> {
|
||||
let user = sqlx::query_as!(Self, "Select * from users where id = ? ", id)
|
||||
.fetch_one(&server_config.db_pool)
|
||||
.await;
|
||||
@ -29,7 +35,7 @@ impl User {
|
||||
///
|
||||
/// # Errors
|
||||
/// fails with [`ServerError`] if the user does not exist or the database cannot be acessed.
|
||||
pub async fn get_user_by_name(
|
||||
async fn get_user_by_name(
|
||||
name: &str,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<Self, ServerError> {
|
||||
@ -39,19 +45,14 @@ impl User {
|
||||
user.map_err(ServerError::Database)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_all_users(
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<Vec<Self>, ServerError> {
|
||||
async fn get_all_users(server_config: &ServerConfig) -> Result<Vec<Self>, ServerError> {
|
||||
let user = sqlx::query_as!(Self, "Select * from users")
|
||||
.fetch_all(&server_config.db_pool)
|
||||
.await;
|
||||
user.map_err(ServerError::Database)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_user(
|
||||
&self,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<(), ServerError> {
|
||||
async fn update_user(&self, server_config: &ServerConfig) -> Result<(), ServerError> {
|
||||
sqlx::query!(
|
||||
"UPDATE users SET
|
||||
username = ?,
|
||||
@ -72,7 +73,7 @@ impl User {
|
||||
///
|
||||
/// # Errors
|
||||
/// fails with [`ServerError`] if the database cannot be acessed. (the user should exist)
|
||||
pub async fn toggle_admin(self, server_config: &ServerConfig) -> Result<(), ServerError> {
|
||||
async fn toggle_admin(self, server_config: &ServerConfig) -> Result<(), ServerError> {
|
||||
let new_role = 2 - (self.role + 1) % 2;
|
||||
sqlx::query!("UPDATE users SET role = ? where id = ?", new_role, self.id)
|
||||
.execute(&server_config.db_pool)
|
||||
@ -80,7 +81,7 @@ impl User {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_language(
|
||||
async fn set_language(
|
||||
self,
|
||||
server_config: &ServerConfig,
|
||||
new_language: &str,
|
||||
@ -101,7 +102,7 @@ impl User {
|
||||
///
|
||||
/// # Errors
|
||||
/// fails with [`ServerError`] if the database cannot be acessed.
|
||||
pub async fn count_admins(server_config: &ServerConfig) -> Result<Count, ServerError> {
|
||||
async fn count_admins(server_config: &ServerConfig) -> Result<Count, ServerError> {
|
||||
let num = sqlx::query_as!(Count, "select count(*) as number from users where role = 2")
|
||||
.fetch_one(&server_config.db_pool)
|
||||
.await?;
|
||||
@ -125,9 +126,9 @@ impl NewUser {
|
||||
username: String,
|
||||
email: String,
|
||||
password: &str,
|
||||
config: &ServerConfig,
|
||||
secret: &Secret,
|
||||
) -> Result<Self, ServerError> {
|
||||
let hash = Self::hash_password(password, config)?;
|
||||
let hash = Self::hash_password(password, secret)?;
|
||||
|
||||
Ok(Self {
|
||||
username,
|
||||
@ -136,14 +137,9 @@ impl NewUser {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn hash_password(
|
||||
password: &str,
|
||||
config: &ServerConfig,
|
||||
) -> Result<String, ServerError> {
|
||||
pub(crate) fn hash_password(password: &str, secret: &Secret) -> Result<String, ServerError> {
|
||||
dotenv().ok();
|
||||
|
||||
let secret = &config.secret;
|
||||
|
||||
let hash = Hasher::default()
|
||||
.with_password(password)
|
||||
.with_secret_key(&secret.secret)
|
||||
@ -179,18 +175,19 @@ pub struct LoginUser {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Link {
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
pub target: String,
|
||||
pub code: String,
|
||||
pub author: i64,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
#[async_trait]
|
||||
pub trait LinkDbOperations<T> {
|
||||
async fn get_link_by_code(code: &str, server_config: &ServerConfig) -> Result<T, ServerError>;
|
||||
async fn delete_link_by_code(
|
||||
code: &str,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<(), ServerError>;
|
||||
async fn update_link(&self, server_config: &ServerConfig) -> Result<(), ServerError>;
|
||||
}
|
||||
|
||||
impl Link {
|
||||
pub(crate) async fn get_link_by_code(
|
||||
#[async_trait]
|
||||
impl LinkDbOperations<Link> for Link {
|
||||
async fn get_link_by_code(
|
||||
code: &str,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<Self, ServerError> {
|
||||
@ -201,7 +198,7 @@ impl Link {
|
||||
link.map_err(ServerError::Database)
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_link_by_code(
|
||||
async fn delete_link_by_code(
|
||||
code: &str,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<(), ServerError> {
|
||||
@ -210,10 +207,7 @@ impl Link {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) async fn update_link(
|
||||
&self,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<(), ServerError> {
|
||||
async fn update_link(&self, server_config: &ServerConfig) -> Result<(), ServerError> {
|
||||
sqlx::query!(
|
||||
"UPDATE links SET
|
||||
title = ?,
|
||||
@ -274,13 +268,6 @@ impl NewLink {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Click {
|
||||
pub id: i64,
|
||||
pub link: i64,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NewClick {
|
||||
pub link: i64,
|
||||
@ -312,8 +299,3 @@ impl NewClick {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Count {
|
||||
pub number: i32,
|
||||
}
|
@ -1,12 +1,22 @@
|
||||
use actix_identity::Identity;
|
||||
use actix_web::web;
|
||||
use enum_map::EnumMap;
|
||||
use serde::Serialize;
|
||||
use tracing::info;
|
||||
use shared::{
|
||||
apirequests::{
|
||||
general::{Filter, Operation, Ordering},
|
||||
links::{LinkOverviewColumns, LinkRequestForm},
|
||||
users::{UserOverviewColumns, UserRequestForm},
|
||||
},
|
||||
datatypes::{Count, FullLink, Link, User},
|
||||
};
|
||||
use sqlx::Row;
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
use super::models::{Count, Link, NewUser, User};
|
||||
use super::models::NewUser;
|
||||
use crate::{
|
||||
forms::LinkForm,
|
||||
models::{NewClick, NewLink},
|
||||
models::{LinkDbOperations, NewClick, NewLink, UserDbOperations},
|
||||
ServerConfig, ServerError,
|
||||
};
|
||||
|
||||
@ -34,12 +44,15 @@ impl Role {
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails only if there are issues using the database.
|
||||
#[instrument(skip(id))]
|
||||
pub async fn authenticate(
|
||||
id: &Identity,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<Role, ServerError> {
|
||||
if let Some(username) = id.identity() {
|
||||
info!("Looking for user {}", username);
|
||||
let user = User::get_user_by_name(&username, server_config).await?;
|
||||
info!("Found user {:?}", user);
|
||||
|
||||
return Ok(match user.role {
|
||||
0 => Role::Disabled,
|
||||
@ -52,32 +65,26 @@ pub async fn authenticate(
|
||||
}
|
||||
|
||||
/// A generic list returntype containing the User and a Vec containing e.g. Links or Users
|
||||
pub struct List<T> {
|
||||
#[derive(Serialize)]
|
||||
pub struct ListWithOwner<T> {
|
||||
pub user: User,
|
||||
pub list: Vec<T>,
|
||||
}
|
||||
|
||||
/// A link together with its author and its click-count.
|
||||
#[derive(Serialize)]
|
||||
pub struct FullLink {
|
||||
link: Link,
|
||||
user: User,
|
||||
clicks: Count,
|
||||
}
|
||||
|
||||
/// Returns a List of `FullLink` meaning `Links` enriched by their author and statistics. This returns all links if the user is either Admin or Regular user.
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails.
|
||||
#[instrument(skip(id))]
|
||||
pub async fn list_all_allowed(
|
||||
id: &Identity,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<List<FullLink>, ServerError> {
|
||||
parameters: LinkRequestForm,
|
||||
) -> Result<ListWithOwner<FullLink>, ServerError> {
|
||||
use crate::sqlx::Row;
|
||||
match authenticate(id, server_config).await? {
|
||||
Role::Admin { user } | Role::Regular { user } => {
|
||||
let links = sqlx::query(
|
||||
"select
|
||||
let mut querystring = "select
|
||||
links.id as lid,
|
||||
links.title as ltitle,
|
||||
links.target as ltarget,
|
||||
@ -93,10 +100,17 @@ pub async fn list_all_allowed(
|
||||
from
|
||||
links
|
||||
join users on links.author = users.id
|
||||
left join clicks on links.id = clicks.link
|
||||
group by
|
||||
links.id",
|
||||
)
|
||||
left join clicks on links.id = clicks.link"
|
||||
.to_string();
|
||||
querystring.push_str(&generate_filter_sql(¶meters.filter));
|
||||
querystring.push_str("\n GROUP BY links.id");
|
||||
if let Some(order) = parameters.order {
|
||||
querystring.push_str(&generate_order_sql(&order));
|
||||
}
|
||||
querystring.push_str(&format!("\n LIMIT {}", parameters.amount));
|
||||
info!("{}", querystring);
|
||||
|
||||
let links = sqlx::query(&querystring)
|
||||
.fetch_all(&server_config.db_pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
@ -123,7 +137,7 @@ pub async fn list_all_allowed(
|
||||
});
|
||||
// show all links
|
||||
let all_links: Vec<FullLink> = links.collect();
|
||||
Ok(List {
|
||||
Ok(ListWithOwner {
|
||||
user,
|
||||
list: all_links,
|
||||
})
|
||||
@ -132,21 +146,107 @@ pub async fn list_all_allowed(
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_filter_sql(filters: &EnumMap<LinkOverviewColumns, Filter>) -> String {
|
||||
let mut result = String::new();
|
||||
let filterstring = filters
|
||||
.iter()
|
||||
.filter_map(|(column, sieve)| {
|
||||
// avoid sql injections
|
||||
let sieve: String = sieve.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
if sieve.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(match column {
|
||||
LinkOverviewColumns::Code => {
|
||||
format!("\n lcode LIKE '%{}%'", sieve)
|
||||
}
|
||||
LinkOverviewColumns::Description => {
|
||||
format!("\n ltitle LIKE '%{}%'", sieve)
|
||||
}
|
||||
LinkOverviewColumns::Target => {
|
||||
format!("\n ltarget LIKE '%{}%'", sieve)
|
||||
}
|
||||
LinkOverviewColumns::Author => {
|
||||
format!("\n usern LIKE '%{}%'", sieve)
|
||||
}
|
||||
LinkOverviewColumns::Statistics => {
|
||||
format!("\n counter LIKE '%{}%'", sieve)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" AND ");
|
||||
if filterstring.len() > 1 {
|
||||
result.push_str("\n WHERE ");
|
||||
result.push_str(&filterstring);
|
||||
}
|
||||
result
|
||||
}
|
||||
macro_rules! ts {
|
||||
($ordering:expr) => {
|
||||
match $ordering {
|
||||
Ordering::Ascending => "ASC",
|
||||
Ordering::Descending => "DESC",
|
||||
};
|
||||
};
|
||||
}
|
||||
fn generate_order_sql(order: &Operation<LinkOverviewColumns, Ordering>) -> String {
|
||||
let filterstring = match order.column {
|
||||
LinkOverviewColumns::Code => {
|
||||
format!("\n ORDER BY lcode {}", ts!(order.value))
|
||||
}
|
||||
LinkOverviewColumns::Description => {
|
||||
format!("\n ORDER BY ltitle {}", ts!(order.value))
|
||||
}
|
||||
LinkOverviewColumns::Target => {
|
||||
format!("\n ORDER BY ltarget {}", ts!(order.value))
|
||||
}
|
||||
LinkOverviewColumns::Author => {
|
||||
format!("\n ORDER BY usern {}", ts!(order.value))
|
||||
}
|
||||
LinkOverviewColumns::Statistics => {
|
||||
format!("\n ORDER BY counter {}", ts!(order.value))
|
||||
}
|
||||
};
|
||||
filterstring
|
||||
}
|
||||
|
||||
/// Only admins can list all users
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions.
|
||||
#[instrument(skip(id))]
|
||||
pub async fn list_users(
|
||||
id: &Identity,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<List<User>, ServerError> {
|
||||
parameters: UserRequestForm,
|
||||
) -> Result<ListWithOwner<User>, ServerError> {
|
||||
match authenticate(id, server_config).await? {
|
||||
Role::Admin { user } => {
|
||||
let all_users: Vec<User> = User::get_all_users(server_config).await?;
|
||||
Ok(List {
|
||||
user,
|
||||
list: all_users,
|
||||
let mut querystring = "Select * from users".to_string();
|
||||
querystring.push_str(&generate_filter_users_sql(¶meters.filter));
|
||||
if let Some(order) = parameters.order {
|
||||
querystring.push_str(&generate_order_users_sql(&order));
|
||||
}
|
||||
querystring.push_str(&format!("\n LIMIT {}", parameters.amount));
|
||||
info!("{}", querystring);
|
||||
|
||||
let users: Vec<User> = sqlx::query(&querystring)
|
||||
.fetch_all(&server_config.db_pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|v| User {
|
||||
id: v.get("id"),
|
||||
username: v.get("username"),
|
||||
email: v.get("email"),
|
||||
password: "invalid".to_owned(),
|
||||
role: v.get("role"),
|
||||
language: v.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(ListWithOwner { user, list: users })
|
||||
}
|
||||
_ => Err(ServerError::User(
|
||||
"Administrator permissions required".to_owned(),
|
||||
@ -154,6 +254,52 @@ pub async fn list_users(
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_filter_users_sql(filters: &EnumMap<UserOverviewColumns, Filter>) -> String {
|
||||
let mut result = String::new();
|
||||
let filterstring = filters
|
||||
.iter()
|
||||
.filter_map(|(column, sieve)| {
|
||||
// avoid sql injections
|
||||
let sieve: String = sieve.chars().filter(|x| x.is_alphanumeric()).collect();
|
||||
if sieve.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(match column {
|
||||
UserOverviewColumns::Id => {
|
||||
format!("\n id LIKE '%{}%'", sieve)
|
||||
}
|
||||
UserOverviewColumns::Username => {
|
||||
format!("\n username LIKE '%{}%'", sieve)
|
||||
}
|
||||
UserOverviewColumns::Email => {
|
||||
format!("\n email LIKE '%{}%'", sieve)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" AND ");
|
||||
if filterstring.len() > 1 {
|
||||
result.push_str("\n WHERE ");
|
||||
result.push_str(&filterstring);
|
||||
}
|
||||
result
|
||||
}
|
||||
fn generate_order_users_sql(order: &Operation<UserOverviewColumns, Ordering>) -> String {
|
||||
let filterstring = match order.column {
|
||||
UserOverviewColumns::Id => {
|
||||
format!("\n ORDER BY id {}", ts!(order.value))
|
||||
}
|
||||
UserOverviewColumns::Username => {
|
||||
format!("\n ORDER BY username {}", ts!(order.value))
|
||||
}
|
||||
UserOverviewColumns::Email => {
|
||||
format!("\n ORDER BY email {}", ts!(order.value))
|
||||
}
|
||||
};
|
||||
filterstring
|
||||
}
|
||||
|
||||
/// A generic returntype containing the User and a single item
|
||||
pub struct Item<T> {
|
||||
pub user: User,
|
||||
@ -165,6 +311,7 @@ pub struct Item<T> {
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions.
|
||||
#[allow(clippy::clippy::missing_panics_doc)]
|
||||
#[instrument(skip(id))]
|
||||
pub async fn get_user(
|
||||
id: &Identity,
|
||||
user_id: &str,
|
||||
@ -198,6 +345,7 @@ pub async fn get_user(
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails.
|
||||
#[instrument()]
|
||||
pub async fn get_user_by_name(
|
||||
username: &str,
|
||||
server_config: &ServerConfig,
|
||||
@ -210,6 +358,7 @@ pub async fn get_user_by_name(
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails, this user does not have permissions or the user already exists.
|
||||
#[instrument(skip(id))]
|
||||
pub async fn create_user(
|
||||
id: &Identity,
|
||||
data: &web::Form<NewUser>,
|
||||
@ -223,7 +372,7 @@ pub async fn create_user(
|
||||
data.username.clone(),
|
||||
data.email.clone(),
|
||||
&data.password,
|
||||
server_config,
|
||||
&server_config.secret,
|
||||
)?;
|
||||
|
||||
new_user.insert_user(server_config).await?;
|
||||
@ -247,6 +396,7 @@ pub async fn create_user(
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails, this user does not have permissions, or the given data is malformed.
|
||||
#[allow(clippy::clippy::missing_panics_doc)]
|
||||
#[instrument(skip(id))]
|
||||
pub async fn update_user(
|
||||
id: &Identity,
|
||||
user_id: &str,
|
||||
@ -261,7 +411,7 @@ pub async fn update_user(
|
||||
Role::Admin { .. } | Role::Regular { .. } => {
|
||||
info!("Updating userinfo: ");
|
||||
let password = if data.password.len() > 3 {
|
||||
NewUser::hash_password(&data.password, server_config)?
|
||||
NewUser::hash_password(&data.password, &server_config.secret)?
|
||||
} else {
|
||||
unmodified_user.password
|
||||
};
|
||||
@ -295,6 +445,7 @@ pub async fn update_user(
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails, this user does not have permissions or the user does not exist.
|
||||
#[instrument(skip(id))]
|
||||
pub async fn toggle_admin(
|
||||
id: &Identity,
|
||||
user_id: &str,
|
||||
@ -333,6 +484,7 @@ pub async fn toggle_admin(
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails, this user does not have permissions or the language given is invalid.
|
||||
#[instrument(skip(id))]
|
||||
pub async fn set_language(
|
||||
id: &Identity,
|
||||
lang_code: &str,
|
||||
@ -347,9 +499,12 @@ pub async fn set_language(
|
||||
Err(ServerError::User("Not Allowed".to_owned()))
|
||||
}
|
||||
},
|
||||
_ => Err(ServerError::User(
|
||||
_ => {
|
||||
warn!("An invalid language was selected!");
|
||||
Err(ServerError::User(
|
||||
"This language is not supported!".to_owned(),
|
||||
)),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,6 +512,7 @@ pub async fn set_language(
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions.
|
||||
#[instrument(skip(id))]
|
||||
pub async fn get_link(
|
||||
id: &Identity,
|
||||
link_code: &str,
|
||||
@ -367,7 +523,10 @@ pub async fn get_link(
|
||||
let link = Link::get_link_by_code(link_code, server_config).await?;
|
||||
Ok(Item { user, item: link })
|
||||
}
|
||||
Role::Disabled | Role::NotAuthenticated => Err(ServerError::User("Not Allowed".to_owned())),
|
||||
Role::Disabled | Role::NotAuthenticated => {
|
||||
warn!("User could not be authenticated!");
|
||||
Err(ServerError::User("Not Allowed".to_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -375,6 +534,7 @@ pub async fn get_link(
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails.
|
||||
#[instrument()]
|
||||
pub async fn get_link_simple(
|
||||
link_code: &str,
|
||||
server_config: &ServerConfig,
|
||||
@ -390,6 +550,7 @@ pub async fn get_link_simple(
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails.
|
||||
#[instrument()]
|
||||
pub async fn click_link(link_id: i64, server_config: &ServerConfig) -> Result<(), ServerError> {
|
||||
info!("Clicking on {:?}", link_id);
|
||||
let new_click = NewClick::new(link_id);
|
||||
@ -401,13 +562,14 @@ pub async fn click_link(link_id: i64, server_config: &ServerConfig) -> Result<()
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions.
|
||||
#[instrument(skip(id))]
|
||||
pub async fn delete_link(
|
||||
id: &Identity,
|
||||
link_code: &str,
|
||||
server_config: &ServerConfig,
|
||||
) -> Result<(), ServerError> {
|
||||
let auth = authenticate(id, server_config).await?;
|
||||
let link = get_link_simple(link_code, server_config).await?;
|
||||
let link: Link = get_link_simple(link_code, server_config).await?;
|
||||
if auth.admin_or_self(link.author) {
|
||||
Link::delete_link_by_code(link_code, server_config).await?;
|
||||
Ok(())
|
||||
@ -420,6 +582,7 @@ pub async fn delete_link(
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions.
|
||||
#[instrument(skip(id))]
|
||||
pub async fn update_link(
|
||||
id: &Identity,
|
||||
link_code: &str,
|
||||
@ -430,7 +593,7 @@ pub async fn update_link(
|
||||
let auth = authenticate(id, server_config).await?;
|
||||
match auth {
|
||||
Role::Admin { .. } | Role::Regular { .. } => {
|
||||
let query = get_link(id, link_code, server_config).await?;
|
||||
let query: Item<Link> = get_link(id, link_code, server_config).await?;
|
||||
if auth.admin_or_self(query.item.author) {
|
||||
let mut link = query.item;
|
||||
let LinkForm {
|
||||
@ -455,6 +618,7 @@ pub async fn update_link(
|
||||
///
|
||||
/// # Errors
|
||||
/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions.
|
||||
#[instrument(skip(id))]
|
||||
pub async fn create_link(
|
||||
id: &Identity,
|
||||
data: web::Form<LinkForm>,
|
||||
@ -469,7 +633,7 @@ pub async fn create_link(
|
||||
info!("Creating link for: {:?}", &new_link);
|
||||
|
||||
new_link.insert(server_config).await?;
|
||||
let new_link = get_link_simple(&code, server_config).await?;
|
||||
let new_link: Link = get_link_simple(&code, server_config).await?;
|
||||
Ok(Item {
|
||||
user,
|
||||
item: new_link,
|
@ -14,8 +14,9 @@ use fluent_templates::LanguageIdentifier;
|
||||
use image::{DynamicImage, ImageOutputFormat, Luma};
|
||||
use qrcode::{render::svg, QrCode};
|
||||
use queries::{authenticate, Role};
|
||||
use shared::apirequests::{links::LinkRequestForm, users::UserRequestForm};
|
||||
use tera::{Context, Tera};
|
||||
use tracing::{info, instrument, trace, warn};
|
||||
use tracing::{error, info, instrument, warn};
|
||||
|
||||
use crate::forms::LinkForm;
|
||||
use crate::models::{LoginUser, NewUser};
|
||||
@ -64,6 +65,34 @@ fn detect_language(request: &HttpRequest) -> Result<String, ServerError> {
|
||||
Ok(languagecode)
|
||||
}
|
||||
|
||||
#[instrument()]
|
||||
pub async fn wasm_app(config: web::Data<crate::ServerConfig>) -> Result<HttpResponse, ServerError> {
|
||||
Ok(HttpResponse::Ok().body(
|
||||
r#"<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<meta name="author" content="Franz Dietrich">
|
||||
<meta http-equiv="robots" content="[noindex|nofollow]">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="stylesheet" href="/static/admin.css">
|
||||
<title>Server integration example</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section id="app"><div class="lds-ellipsis">Loading: <div></div><div></div><div></div><div></div></div></section>
|
||||
<script type="module">
|
||||
import init from '/app/pkg/package.js';
|
||||
init('/app/pkg/package_bg.wasm');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>"#,
|
||||
))
|
||||
}
|
||||
|
||||
/// Show the list of all available links if a user is authenticated
|
||||
|
||||
#[instrument(skip(id, tera))]
|
||||
@ -72,17 +101,38 @@ pub async fn index(
|
||||
config: web::Data<crate::ServerConfig>,
|
||||
id: Identity,
|
||||
) -> Result<HttpResponse, ServerError> {
|
||||
if let Ok(links) = queries::list_all_allowed(&id, &config).await {
|
||||
match queries::list_all_allowed(&id, &config, LinkRequestForm::default()).await {
|
||||
Ok(links) => {
|
||||
let mut data = Context::new();
|
||||
data.insert("user", &links.user);
|
||||
data.insert("title", &format!("Links der {}", &config.brand_name,));
|
||||
data.insert("links_per_users", &links.list);
|
||||
let rendered = tera.render("index.html", &data)?;
|
||||
Ok(HttpResponse::Ok().body(rendered))
|
||||
} else {
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get the links: {:?}", e);
|
||||
Ok(redirect_builder("/admin/login/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(id))]
|
||||
pub async fn index_json(
|
||||
config: web::Data<crate::ServerConfig>,
|
||||
form: web::Json<LinkRequestForm>,
|
||||
id: Identity,
|
||||
) -> Result<HttpResponse, ServerError> {
|
||||
info!("Listing Links to Json api");
|
||||
match queries::list_all_allowed(&id, &config, form.0).await {
|
||||
Ok(links) => Ok(HttpResponse::Ok().json2(&links.list)),
|
||||
Err(e) => {
|
||||
error!("Failed to access database: {:?}", e);
|
||||
warn!("Not logged in - redirecting to login page");
|
||||
Ok(redirect_builder("/admin/login/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the list of all available links if a user is authenticated
|
||||
#[instrument(skip(id, tera))]
|
||||
@ -91,7 +141,7 @@ pub async fn index_users(
|
||||
config: web::Data<crate::ServerConfig>,
|
||||
id: Identity,
|
||||
) -> Result<HttpResponse, ServerError> {
|
||||
if let Ok(users) = queries::list_users(&id, &config).await {
|
||||
if let Ok(users) = queries::list_users(&id, &config, UserRequestForm::default()).await {
|
||||
let mut data = Context::new();
|
||||
data.insert("user", &users.user);
|
||||
data.insert("title", &format!("Benutzer der {}", &config.brand_name,));
|
||||
@ -103,6 +153,19 @@ pub async fn index_users(
|
||||
Ok(redirect_builder("/admin/login"))
|
||||
}
|
||||
}
|
||||
#[instrument(skip(id))]
|
||||
pub async fn index_users_json(
|
||||
config: web::Data<crate::ServerConfig>,
|
||||
form: web::Json<UserRequestForm>,
|
||||
id: Identity,
|
||||
) -> Result<HttpResponse, ServerError> {
|
||||
info!("Listing Users to Json api");
|
||||
if let Ok(users) = queries::list_users(&id, &config, form.0).await {
|
||||
Ok(HttpResponse::Ok().json2(&users.list))
|
||||
} else {
|
||||
Ok(redirect_builder("/admin/login"))
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(id, tera))]
|
||||
pub async fn view_link_empty(
|
||||
@ -323,7 +386,7 @@ pub async fn login(
|
||||
if let Ok(r) = authenticate(&id, &config).await {
|
||||
match r {
|
||||
Role::Admin { user } | Role::Regular { user } => {
|
||||
trace!(
|
||||
info!(
|
||||
"This user ({}) is already logged in redirecting to /admin/index/",
|
||||
user.username
|
||||
);
|
||||
@ -380,7 +443,7 @@ pub async fn logout(id: Identity) -> Result<HttpResponse, ServerError> {
|
||||
Ok(redirect_builder("/admin/login/"))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
#[instrument(skip(tera))]
|
||||
pub async fn redirect(
|
||||
tera: web::Data<Tera>,
|
||||
config: web::Data<crate::ServerConfig>,
|
163
pslink/static/admin.css
Normal file
163
pslink/static/admin.css
Normal file
@ -0,0 +1,163 @@
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
}
|
||||
.center {
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -200px;
|
||||
margin-top: -200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 30px;
|
||||
color: #333;
|
||||
}
|
||||
.center input {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
margin: 5px;
|
||||
border-radius: 1px;
|
||||
border: 1px solid rgb(90, 90, 90);
|
||||
font-family: inherit;
|
||||
background-color: #eae9ea;
|
||||
}
|
||||
|
||||
.center {
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
margin-left: -400px;
|
||||
margin-top: -300px;
|
||||
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: center;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: #eee;
|
||||
}
|
||||
table tr:nth-child(odd) {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
table tr.filters input {
|
||||
background-image: url("/static/search.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
table tr.filters td {
|
||||
padding:5px;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
align-items:stretch;
|
||||
width:100%;
|
||||
height: 60px;
|
||||
}
|
||||
nav ol {
|
||||
display:flex;
|
||||
align-items: center;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
nav li a, nav li div.willkommen {
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
nav li a {
|
||||
background: rgb(2,0,36);
|
||||
background: linear-gradient(180deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 35%, rgb(0, 145, 174) 100%);
|
||||
}
|
||||
|
||||
nav li {
|
||||
float: left;
|
||||
}
|
||||
nav li a:hover {
|
||||
background: rgb(2,0,36);
|
||||
background: linear-gradient(180deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 35%, rgb(60, 170, 255) 100%);
|
||||
}
|
||||
|
||||
nav li {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
nav li div{
|
||||
background-color: burlywood;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
|
||||
svg {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
div.actions {
|
||||
margin-left:5px;
|
||||
display: flex;
|
||||
width:100%;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
color: #333;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
div.danger {
|
||||
background-color: rgb(235, 127, 77);
|
||||
font-size: smaller;
|
||||
border: 2px solid crimson;
|
||||
}
|
||||
|
||||
div.danger h3 {
|
||||
width:100%;
|
||||
}
|
||||
|
||||
a.button, div.actions input {
|
||||
width: 250px;
|
||||
display:block;
|
||||
padding: 15px;
|
||||
margin-left: 15px;
|
||||
text-align: center;
|
||||
border-radius: 1px;
|
||||
border: 1px solid rgb(90, 90, 90);
|
||||
font-family: inherit;
|
||||
background-color: #eae9ea;
|
||||
}
|
1
pslink/static/search.svg
Normal file
1
pslink/static/search.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><svg height="32px" version="1.1" viewBox="0 0 32 32" width="32px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><desc/><defs/><g fill="none" fill-rule="evenodd" id="Page-1" stroke="none" stroke-width="1"><g fill="#929292" id="icon-111-search"><path d="M19.4271164,20.4271164 C18.0372495,21.4174803 16.3366522,22 14.5,22 C9.80557939,22 6,18.1944206 6,13.5 C6,8.80557939 9.80557939,5 14.5,5 C19.1944206,5 23,8.80557939 23,13.5 C23,15.8472103 22.0486052,17.9722103 20.5104077,19.5104077 L26.5077736,25.5077736 C26.782828,25.782828 26.7761424,26.2238576 26.5,26.5 C26.2219324,26.7780676 25.7796227,26.7796227 25.5077736,26.5077736 L19.4271164,20.4271164 L19.4271164,20.4271164 Z M14.5,21 C18.6421358,21 22,17.6421358 22,13.5 C22,9.35786417 18.6421358,6 14.5,6 C10.3578642,6 7,9.35786417 7,13.5 C7,17.6421358 10.3578642,21 14.5,21 L14.5,21 Z" id="search"/></g></g></svg>
|
After Width: | Height: | Size: 927 B |
95
pslink/static/style.css
Normal file
95
pslink/static/style.css
Normal file
@ -0,0 +1,95 @@
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin:0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.center {
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -200px;
|
||||
margin-top: -200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 30px;
|
||||
color: #333;
|
||||
}
|
||||
.center input {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
margin: 5px;
|
||||
border-radius: 1px;
|
||||
border: 1px solid rgb(90, 90, 90);
|
||||
font-family: inherit;
|
||||
background-color: #eae9ea;
|
||||
}
|
||||
|
||||
.lds-ellipsis {
|
||||
display: block;
|
||||
position: fixed;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -40px;
|
||||
margin-left: -40px;
|
||||
color:rgb(130, 0, 0);
|
||||
}
|
||||
.lds-ellipsis div {
|
||||
position: absolute;
|
||||
top: 33px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
background: rgb(130, 0, 0);
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
}
|
||||
.lds-ellipsis div:nth-child(1) {
|
||||
left: 8px;
|
||||
animation: lds-ellipsis1 0.6s infinite;
|
||||
}
|
||||
.lds-ellipsis div:nth-child(2) {
|
||||
left: 8px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
.lds-ellipsis div:nth-child(3) {
|
||||
left: 32px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
.lds-ellipsis div:nth-child(4) {
|
||||
left: 56px;
|
||||
animation: lds-ellipsis3 0.6s infinite;
|
||||
}
|
||||
@keyframes lds-ellipsis1 {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes lds-ellipsis3 {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@keyframes lds-ellipsis2 {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(24px, 0);
|
||||
}
|
||||
}
|
16
shared/Cargo.toml
Normal file
16
shared/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
authors = ["Dietrich <dietrich@teilgedanken.de>"]
|
||||
categories = ["web-programming", "network-programming", "web-programming::http-server", "command-line-utilities"]
|
||||
description = "A simple webservice that allows registered users to create short links including qr-codes.\nAnyone can visit the shortened links. This is an ideal setup for small busines or for publishing papers."
|
||||
edition = "2018"
|
||||
keywords = ["url", "link", "webpage", "actix", "web"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
name = "shared"
|
||||
readme = "../pslink/README.md"
|
||||
repository = "https://github.com/enaut/pslink/"
|
||||
version = "0.3.1"
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
chrono = {version = "0.4", features = ["serde"] }
|
||||
enum-map = {version="1", features = ["serde"]}
|
27
shared/src/apirequests/general.rs
Normal file
27
shared/src/apirequests/general.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, Default)]
|
||||
pub struct Filter {
|
||||
pub sieve: String,
|
||||
}
|
||||
|
||||
impl Deref for Filter {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.sieve
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Debug)]
|
||||
pub enum Ordering {
|
||||
Ascending,
|
||||
Descending,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||
pub struct Operation<T, V> {
|
||||
pub column: T,
|
||||
pub value: V,
|
||||
}
|
31
shared/src/apirequests/links.rs
Normal file
31
shared/src/apirequests/links.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use enum_map::{Enum, EnumMap};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::general::{Filter, Operation, Ordering};
|
||||
|
||||
/// A generic list returntype containing the User and a Vec containing e.g. Links or Users
|
||||
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||
pub struct LinkRequestForm {
|
||||
pub filter: EnumMap<LinkOverviewColumns, Filter>,
|
||||
pub order: Option<Operation<LinkOverviewColumns, Ordering>>,
|
||||
pub amount: usize,
|
||||
}
|
||||
|
||||
impl Default for LinkRequestForm {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
filter: EnumMap::default(),
|
||||
order: None,
|
||||
amount: 20,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Enum)]
|
||||
pub enum LinkOverviewColumns {
|
||||
Code,
|
||||
Description,
|
||||
Target,
|
||||
Author,
|
||||
Statistics,
|
||||
}
|
3
shared/src/apirequests/mod.rs
Normal file
3
shared/src/apirequests/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod general;
|
||||
pub mod links;
|
||||
pub mod users;
|
28
shared/src/apirequests/users.rs
Normal file
28
shared/src/apirequests/users.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use enum_map::{Enum, EnumMap};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::general::{Filter, Operation, Ordering};
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||
pub struct UserRequestForm {
|
||||
pub filter: EnumMap<UserOverviewColumns, Filter>,
|
||||
pub order: Option<Operation<UserOverviewColumns, Ordering>>,
|
||||
pub amount: usize,
|
||||
}
|
||||
|
||||
impl Default for UserRequestForm {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
filter: EnumMap::default(),
|
||||
order: None,
|
||||
amount: 20,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Enum)]
|
||||
pub enum UserOverviewColumns {
|
||||
Id,
|
||||
Email,
|
||||
Username,
|
||||
}
|
46
shared/src/datatypes.rs
Normal file
46
shared/src/datatypes.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
/// A generic list returntype containing the User and a Vec containing e.g. Links or Users
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct ListWithOwner<T> {
|
||||
pub user: User,
|
||||
pub list: Vec<T>,
|
||||
}
|
||||
|
||||
/// A link together with its author and its click-count.
|
||||
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||
pub struct FullLink {
|
||||
pub link: Link,
|
||||
pub user: User,
|
||||
pub clicks: Count,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub role: i64,
|
||||
pub language: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Link {
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
pub target: String,
|
||||
pub code: String,
|
||||
pub author: i64,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Count {
|
||||
pub number: i32,
|
||||
}
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Click {
|
||||
pub id: i64,
|
||||
pub link: i64,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
}
|
2
shared/src/lib.rs
Normal file
2
shared/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod apirequests;
|
||||
pub mod datatypes;
|
@ -1,99 +0,0 @@
|
||||
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
}
|
||||
.center {
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
margin-left: -400px;
|
||||
margin-top: -300px;
|
||||
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: center;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: #eee;
|
||||
}
|
||||
table tr:nth-child(odd) {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
nav ol {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: #333;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
nav li a, nav li div.willkommen {
|
||||
display: block;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav li {
|
||||
float: left;
|
||||
}
|
||||
nav li a:hover {
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
nav li {
|
||||
border-right: 1px solid #bbb;
|
||||
}
|
||||
|
||||
nav li:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
svg {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
div.actions {
|
||||
margin-left:5px;
|
||||
display: flex;
|
||||
width:100%;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
color: #333;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
div.danger {
|
||||
background-color: rgb(235, 127, 77);
|
||||
font-size: smaller;
|
||||
border: 2px solid crimson;
|
||||
}
|
||||
|
||||
div.danger h3 {
|
||||
width:100%;
|
||||
}
|
||||
|
||||
a.button, div.actions input {
|
||||
width: 250px;
|
||||
display:block;
|
||||
padding: 15px;
|
||||
margin-left: 15px;
|
||||
text-align: center;
|
||||
border-radius: 1px;
|
||||
border: 1px solid rgb(90, 90, 90);
|
||||
font-family: inherit;
|
||||
background-color: #eae9ea;
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin:0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.center {
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -200px;
|
||||
margin-top: -200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 30px;
|
||||
color: #333;
|
||||
}
|
||||
.center input {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
margin: 5px;
|
||||
border-radius: 1px;
|
||||
border: 1px solid rgb(90, 90, 90);
|
||||
font-family: inherit;
|
||||
background-color: #eae9ea;
|
||||
}
|
Loading…
Reference in New Issue
Block a user