Compare commits
No commits in common. "rear_auth" and "main" have entirely different histories.
298
Cargo.lock
generated
298
Cargo.lock
generated
@ -131,24 +131,6 @@ version = "1.0.75"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "argon2"
|
|
||||||
version = "0.5.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
|
||||||
dependencies = [
|
|
||||||
"base64ct",
|
|
||||||
"blake2",
|
|
||||||
"cpufeatures",
|
|
||||||
"password-hash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arrayref"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
@ -335,9 +317,9 @@ checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.80"
|
version = "0.1.77"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
|
checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -476,43 +458,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "axum-login"
|
|
||||||
version = "0.15.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4012877d9672b7902aa6567960208756f68a09de81e988fa18fe369e92f90471"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"axum 0.7.4",
|
|
||||||
"form_urlencoded",
|
|
||||||
"serde",
|
|
||||||
"subtle",
|
|
||||||
"thiserror",
|
|
||||||
"tower-cookies",
|
|
||||||
"tower-layer",
|
|
||||||
"tower-service",
|
|
||||||
"tower-sessions",
|
|
||||||
"tracing",
|
|
||||||
"urlencoding",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "axum-messages"
|
|
||||||
version = "0.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c0d06050ddcb05eaaffe44c5f73443ea936eadae6a8332dd47b8c8c8cb1af98a"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"axum-core 0.4.3",
|
|
||||||
"http 1.0.0",
|
|
||||||
"parking_lot",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tower",
|
|
||||||
"tower-sessions-core",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.69"
|
version = "0.3.69"
|
||||||
@ -540,12 +485,6 @@ version = "0.21.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "base64"
|
|
||||||
version = "0.22.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@ -590,28 +529,6 @@ dependencies = [
|
|||||||
"wyz",
|
"wyz",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "blake2"
|
|
||||||
version = "0.10.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
|
||||||
dependencies = [
|
|
||||||
"digest",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "blake3"
|
|
||||||
version = "1.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52"
|
|
||||||
dependencies = [
|
|
||||||
"arrayref",
|
|
||||||
"arrayvec",
|
|
||||||
"cc",
|
|
||||||
"cfg-if",
|
|
||||||
"constant_time_eq",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@ -808,23 +725,6 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "constant_time_eq"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cookie"
|
|
||||||
version = "0.18.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
|
||||||
dependencies = [
|
|
||||||
"percent-encoding",
|
|
||||||
"time",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@ -843,9 +743,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.12"
|
version = "0.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
|
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@ -1118,9 +1018,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.1"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
@ -1226,17 +1126,6 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-macro"
|
|
||||||
version = "0.3.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.48",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.29"
|
version = "0.3.29"
|
||||||
@ -1258,7 +1147,6 @@ dependencies = [
|
|||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-macro",
|
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
@ -1293,10 +1181,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
|
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1764,7 +1650,6 @@ checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1839,9 +1724,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minijinja"
|
name = "minijinja"
|
||||||
version = "2.0.3"
|
version = "1.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "933ee10775d58fca8238a84fe165dfe4bde8b07d7574f24d76ffea91170f3ac6"
|
checksum = "431d72874542d43aba1ca605870eacab134fdeb0c8fe27666ecf4b2662239df2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memo-map",
|
"memo-map",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@ -1851,9 +1736,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minijinja-autoreload"
|
name = "minijinja-autoreload"
|
||||||
version = "2.0.3"
|
version = "1.0.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bfe3362301b5f450f0f07175cc8cacdd9edb352c7d0375af72646dfb5769fc2a"
|
checksum = "dbe548b8e2b0590e25f0baf95f76c1d7ea73ca264f1f90fe1bf6e00cc6612d19"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"minijinja",
|
"minijinja",
|
||||||
"notify",
|
"notify",
|
||||||
@ -1872,8 +1757,6 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum 0.7.4",
|
"axum 0.7.4",
|
||||||
"axum-login",
|
|
||||||
"axum-messages",
|
|
||||||
"barrel",
|
"barrel",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
@ -1886,17 +1769,14 @@ dependencies = [
|
|||||||
"minijinja-autoreload",
|
"minijinja-autoreload",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rear",
|
"rear",
|
||||||
"rear_auth",
|
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"slug",
|
"slug",
|
||||||
"sqlformat",
|
"sqlformat",
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-sessions",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2005,12 +1885,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-conv"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.45"
|
version = "0.1.45"
|
||||||
@ -2173,29 +2047,6 @@ dependencies = [
|
|||||||
"windows-targets 0.48.5",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "password-auth"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1a2a4764cc1f8d961d802af27193c6f4f0124bd0e76e8393cf818e18880f0524"
|
|
||||||
dependencies = [
|
|
||||||
"argon2",
|
|
||||||
"getrandom",
|
|
||||||
"password-hash",
|
|
||||||
"rand_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "password-hash"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
|
||||||
dependencies = [
|
|
||||||
"base64ct",
|
|
||||||
"rand_core",
|
|
||||||
"subtle",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
@ -2394,9 +2245,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark"
|
name = "pulldown-cmark"
|
||||||
version = "0.11.0"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8746739f11d39ce5ad5c2520a9b75285310dbfe78c541ccf832d38615765aec0"
|
checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.0",
|
"bitflags 2.4.0",
|
||||||
"getopts",
|
"getopts",
|
||||||
@ -2407,9 +2258,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark-escape"
|
name = "pulldown-cmark-escape"
|
||||||
version = "0.11.0"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
checksum = "d5d8f9aa0e3cbcfaf8bf00300004ee3b72f74770f9cbac93f6928771f613276b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
@ -2470,33 +2321,10 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"slug",
|
"slug",
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rear_auth"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"async-trait",
|
|
||||||
"axum 0.7.4",
|
|
||||||
"axum-login",
|
|
||||||
"axum-messages",
|
|
||||||
"blake3",
|
|
||||||
"chrono",
|
|
||||||
"entity",
|
|
||||||
"log",
|
|
||||||
"password-auth",
|
|
||||||
"rear",
|
|
||||||
"sea-orm",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
@ -2935,18 +2763,18 @@ checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.203"
|
version = "1.0.188"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
|
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.203"
|
version = "1.0.188"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
|
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3229,7 +3057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4"
|
checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64 0.21.5",
|
"base64",
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"bitflags 2.4.0",
|
"bitflags 2.4.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
@ -3276,7 +3104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24"
|
checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64 0.21.5",
|
"base64",
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"bitflags 2.4.0",
|
"bitflags 2.4.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
@ -3436,18 +3264,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.61"
|
version = "1.0.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.61"
|
version = "1.0.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3466,13 +3294,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.36"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
"num-conv",
|
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
"serde",
|
||||||
"time-core",
|
"time-core",
|
||||||
@ -3487,11 +3314,10 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.18"
|
version = "0.2.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-conv",
|
|
||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3598,23 +3424,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-cookies"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"axum-core 0.4.3",
|
|
||||||
"cookie",
|
|
||||||
"futures-util",
|
|
||||||
"http 1.0.0",
|
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
|
||||||
"tower-layer",
|
|
||||||
"tower-service",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-http"
|
name = "tower-http"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@ -3644,57 +3453,6 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-sessions"
|
|
||||||
version = "0.12.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c2d9b6f0c4938eed0eefd9cce19319b4bdad10e11ca9d8c3be373ce734bbfd63"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"http 1.0.0",
|
|
||||||
"time",
|
|
||||||
"tokio",
|
|
||||||
"tower-cookies",
|
|
||||||
"tower-layer",
|
|
||||||
"tower-service",
|
|
||||||
"tower-sessions-core",
|
|
||||||
"tower-sessions-memory-store",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-sessions-core"
|
|
||||||
version = "0.12.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "38767064990c327ec1d92bba2576dce0944750e9c9ae021f12ebc72de77ac406"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"axum-core 0.4.3",
|
|
||||||
"base64 0.22.1",
|
|
||||||
"futures",
|
|
||||||
"http 1.0.0",
|
|
||||||
"parking_lot",
|
|
||||||
"rand",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"time",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-sessions-memory-store"
|
|
||||||
version = "0.12.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a8b09bbe2c138a9b0ebf307dc6e6a4f7723c59545e0f4fe5e329a89868164ae3"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"time",
|
|
||||||
"tokio",
|
|
||||||
"tower-sessions-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.40"
|
version = "0.1.40"
|
||||||
|
11
Cargo.toml
11
Cargo.toml
@ -9,7 +9,7 @@ default-run = "miniweb"
|
|||||||
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "entity", "migration", "rear", "rear_auth"]
|
members = [".", "entity", "migration", "rear"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# https://github.com/rust-db/barrel/blob/master/guides/diesel-setup.md
|
# https://github.com/rust-db/barrel/blob/master/guides/diesel-setup.md
|
||||||
@ -20,7 +20,6 @@ default = ["use_barrel"]
|
|||||||
# strinto = { path = "./strinto" }
|
# strinto = { path = "./strinto" }
|
||||||
entity = { path = "./entity" }
|
entity = { path = "./entity" }
|
||||||
rear = { path = "./rear" }
|
rear = { path = "./rear" }
|
||||||
rear_auth = { path = "./rear_auth" }
|
|
||||||
sea-orm = { version = "0.12.10", features = [
|
sea-orm = { version = "0.12.10", features = [
|
||||||
"runtime-tokio-native-tls",
|
"runtime-tokio-native-tls",
|
||||||
"sqlx-postgres",
|
"sqlx-postgres",
|
||||||
@ -28,18 +27,16 @@ sea-orm = { version = "0.12.10", features = [
|
|||||||
sqlformat = { version = "0.2.2", optional = true }
|
sqlformat = { version = "0.2.2", optional = true }
|
||||||
anyhow = "1.0.75"
|
anyhow = "1.0.75"
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
axum-login = "0.15.3"
|
|
||||||
axum-messages = "0.6.1"
|
|
||||||
barrel = { version = "0.7.0", optional = true, features = ["pg"] }
|
barrel = { version = "0.7.0", optional = true, features = ["pg"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
mime_guess = "2.0.4"
|
mime_guess = "2.0.4"
|
||||||
minijinja = { version = "2.0.3", features = [
|
minijinja = { version = "1.0.11", features = [
|
||||||
"loader",
|
"loader",
|
||||||
"builtins",
|
"builtins",
|
||||||
"urlencode",
|
"urlencode",
|
||||||
"deserialization",
|
"deserialization",
|
||||||
] }
|
] }
|
||||||
minijinja-autoreload = "2.0.3"
|
minijinja-autoreload = "1.0.8"
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
rust-embed = { version = "8.0.0", features = [
|
rust-embed = { version = "8.0.0", features = [
|
||||||
"axum",
|
"axum",
|
||||||
@ -57,5 +54,3 @@ async-trait = "0.1.77"
|
|||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tower-http = { version = "0.5.1", features = ["trace"] }
|
tower-http = { version = "0.5.1", features = ["trace"] }
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
thiserror = "1.0.61"
|
|
||||||
tower-sessions = { version = "0.12.2" }
|
|
||||||
|
6
TODOS.md
6
TODOS.md
@ -63,10 +63,4 @@ These errors should encompass:
|
|||||||
- Wrong Data / Not Authorized and other 400 base
|
- Wrong Data / Not Authorized and other 400 base
|
||||||
- Not Found Error: for 404s
|
- Not Found Error: for 404s
|
||||||
- Internal Errors: for 500s
|
- Internal Errors: for 500s
|
||||||
|
|
||||||
Repository functions also need to be redesigned to be Results.
|
|
||||||
|
|
||||||
Finally, RepositoryResponse might require us to add abilities to
|
|
||||||
- render your own html ?
|
|
||||||
- at least influence HX Response Headers for htmx support
|
|
||||||
|
|
@ -3,17 +3,18 @@ name = "entity"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
default-run = "entity"
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "entity"
|
name = "entity"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
tokio = { version = "1.32.0", features = ["full"] }
|
tokio = { version = "1.32.0", features = ["full"] }
|
||||||
|
|
||||||
[dependencies.sea-orm]
|
[dependencies.sea-orm]
|
||||||
version = "0.12.10" # sea-orm version
|
version = "0.12.10" # sea-orm version
|
||||||
features = ["runtime-tokio-native-tls", "sqlx-postgres"]
|
features = [
|
||||||
|
"runtime-tokio-native-tls",
|
||||||
|
"sqlx-postgres",
|
||||||
|
]
|
@ -1,50 +0,0 @@
|
|||||||
# Entities for rear
|
|
||||||
|
|
||||||
## User
|
|
||||||
|
|
||||||
Should be compatible to Django-user to be able to easily share the db
|
|
||||||
|
|
||||||
```
|
|
||||||
class models.User
|
|
||||||
User objects have the following fields:
|
|
||||||
|
|
||||||
username¶
|
|
||||||
Required. 150 characters or fewer. Usernames may contain alphanumeric, _, @, +, . and - characters.
|
|
||||||
|
|
||||||
The max_length should be sufficient for many use cases. If you need a longer length, please use a custom user model. If you use MySQL with the utf8mb4 encoding (recommended for proper Unicode support), specify at most max_length=191 because MySQL can only create unique indexes with 191 characters in that case by default.
|
|
||||||
|
|
||||||
first_name¶
|
|
||||||
Optional (blank=True). 150 characters or fewer.
|
|
||||||
|
|
||||||
last_name¶
|
|
||||||
Optional (blank=True). 150 characters or fewer.
|
|
||||||
|
|
||||||
email¶
|
|
||||||
Optional (blank=True). Email address.
|
|
||||||
|
|
||||||
password¶
|
|
||||||
Required. A hash of, and metadata about, the password. (Django doesn’t store the raw password.) Raw passwords can be arbitrarily long and can contain any character. See the password documentation.
|
|
||||||
|
|
||||||
groups¶
|
|
||||||
Many-to-many relationship to Group
|
|
||||||
|
|
||||||
user_permissions¶
|
|
||||||
Many-to-many relationship to Permission
|
|
||||||
|
|
||||||
is_staff¶
|
|
||||||
Boolean. Allows this user to access the admin site.
|
|
||||||
|
|
||||||
is_active¶
|
|
||||||
Boolean. Marks this user account as active. We recommend that you set this flag to False instead of deleting accounts. That way, if your applications have any foreign keys to users, the foreign keys won’t break.
|
|
||||||
|
|
||||||
This doesn’t necessarily control whether or not the user can log in. Authentication backends aren’t required to check for the is_active flag but the default backend (ModelBackend) and the RemoteUserBackend do. You can use AllowAllUsersModelBackend or AllowAllUsersRemoteUserBackend if you want to allow inactive users to login. In this case, you’ll also want to customize the AuthenticationForm used by the LoginView as it rejects inactive users. Be aware that the permission-checking methods such as has_perm() and the authentication in the Django admin all return False for inactive users.
|
|
||||||
|
|
||||||
is_superuser¶
|
|
||||||
Boolean. Treats this user as having all permissions without assigning any permission to it in particular.
|
|
||||||
|
|
||||||
last_login¶
|
|
||||||
A datetime of the user’s last login.
|
|
||||||
|
|
||||||
date_joined¶
|
|
||||||
The date/time when the account was created.
|
|
||||||
```
|
|
@ -1,43 +0,0 @@
|
|||||||
use sea_orm::{
|
|
||||||
ConnectionTrait, Database, DatabaseConnection, DbConn, EntityTrait, Schema, Statement,
|
|
||||||
};
|
|
||||||
|
|
||||||
use entity::{group, group_permission, permission, user, user_group, user_permission};
|
|
||||||
|
|
||||||
async fn drop_table<E>(db: &DatabaseConnection, entity: E)
|
|
||||||
where
|
|
||||||
E: EntityTrait,
|
|
||||||
{
|
|
||||||
let table_name = entity.table_name();
|
|
||||||
let sql = format!("DROP TABLE IF EXISTS {} CASCADE;", table_name.to_string());
|
|
||||||
|
|
||||||
println!("{}", sql);
|
|
||||||
|
|
||||||
let stmt = Statement::from_string(db.get_database_backend(), sql);
|
|
||||||
|
|
||||||
match db.execute(stmt).await {
|
|
||||||
Ok(_) => println!("Dropped table {}", table_name),
|
|
||||||
Err(e) => println!("Error dropping table {}: {}", table_name, e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn drop_tables(db: &DbConn) {
|
|
||||||
drop_table(db, user::Entity).await;
|
|
||||||
drop_table(db, permission::Entity).await;
|
|
||||||
drop_table(db, group::Entity).await;
|
|
||||||
drop_table(db, user_permission::Entity).await;
|
|
||||||
drop_table(db, group_permission::Entity).await;
|
|
||||||
drop_table(db, user_group::Entity).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
// Running Entities manually creates the tables from the entities in their latest incarnation.
|
|
||||||
println!("Connecting to database...");
|
|
||||||
let db: DatabaseConnection =
|
|
||||||
Database::connect("postgresql://miniweb:miniweb@localhost:54321/miniweb")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
println!("Dropping tables for entities...");
|
|
||||||
drop_tables(&db).await;
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
|
||||||
#[sea_orm(table_name = "groups")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key)]
|
|
||||||
#[serde(skip_deserializing)]
|
|
||||||
pub id: i32,
|
|
||||||
#[sea_orm(index = "group_names")]
|
|
||||||
pub name: String,
|
|
||||||
// permissions: many-to-many to Permission.
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1,65 +0,0 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
|
|
||||||
pub struct Entity;
|
|
||||||
|
|
||||||
impl EntityName for Entity {
|
|
||||||
fn table_name(&self) -> &str {
|
|
||||||
"group_permission"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveModel, DeriveActiveModel)]
|
|
||||||
pub struct Model {
|
|
||||||
pub group_id: i32,
|
|
||||||
pub permission_id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
|
||||||
pub enum Column {
|
|
||||||
GroupId,
|
|
||||||
PermissionId,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
|
|
||||||
pub enum PrimaryKey {
|
|
||||||
GroupId,
|
|
||||||
PermissionId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimaryKeyTrait for PrimaryKey {
|
|
||||||
type ValueType = (i32, i32);
|
|
||||||
|
|
||||||
fn auto_increment() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColumnTrait for Column {
|
|
||||||
type EntityName = Entity;
|
|
||||||
|
|
||||||
fn def(&self) -> ColumnDef {
|
|
||||||
match self {
|
|
||||||
Self::GroupId => ColumnType::Integer.def(),
|
|
||||||
Self::PermissionId => ColumnType::Integer.def(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::group::Entity",
|
|
||||||
from = "Column::GroupId",
|
|
||||||
to = "super::group::Column::Id"
|
|
||||||
)]
|
|
||||||
Group,
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::permission::Entity",
|
|
||||||
from = "Column::PermissionId",
|
|
||||||
to = "super::permission::Column::Id"
|
|
||||||
)]
|
|
||||||
Permission,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1,13 +1,5 @@
|
|||||||
pub mod group;
|
|
||||||
pub mod group_permission;
|
|
||||||
pub mod permission;
|
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod user_group;
|
pub mod permission;
|
||||||
pub mod user_permission;
|
|
||||||
|
|
||||||
pub use group::Entity as Group;
|
|
||||||
pub use group_permission::Entity as GroupPermission;
|
|
||||||
pub use permission::Entity as Permission;
|
|
||||||
pub use user::Entity as User;
|
pub use user::Entity as User;
|
||||||
pub use user_group::Entity as UserGroup;
|
pub use permission::Entity as Permission;
|
||||||
pub use user_permission::Entity as UserPermission;
|
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
use sea_orm::{ConnectionTrait, Database, DatabaseConnection, DbConn, EntityTrait, Schema};
|
use sea_orm::{DbConn, EntityTrait, Schema, DatabaseConnection, Database, ConnectionTrait};
|
||||||
|
|
||||||
mod group;
|
|
||||||
mod group_permission;
|
|
||||||
mod permission;
|
|
||||||
mod user;
|
mod user;
|
||||||
mod user_group;
|
mod permission;
|
||||||
mod user_permission;
|
|
||||||
|
|
||||||
async fn create_table<E>(db: &DbConn, entity: E)
|
async fn create_table<E>(db: &DbConn, entity: E)
|
||||||
where
|
where
|
||||||
@ -16,12 +12,12 @@ where
|
|||||||
|
|
||||||
let mut table_create_statement = schema.create_table_from_entity(entity);
|
let mut table_create_statement = schema.create_table_from_entity(entity);
|
||||||
// we need to shadow the mutable instance X, because if_not_exists() returns &mut X
|
// we need to shadow the mutable instance X, because if_not_exists() returns &mut X
|
||||||
let table_create_statement = table_create_statement.if_not_exists();
|
let table_create_statement = table_create_statement.if_not_exists();
|
||||||
// we need to reborrow after dereferencing, which transforms our &mut X into &X
|
// we need to reborrow after dereferencing, which transforms our &mut X into &X
|
||||||
let stmt = backend.build(&*table_create_statement);
|
let stmt = backend.build(&*table_create_statement);
|
||||||
|
|
||||||
match db.execute(stmt).await {
|
match db.execute(stmt).await {
|
||||||
Ok(_) => println!("Created {}", entity.table_name()),
|
Ok(_) => println!("Migrated {}", entity.table_name()),
|
||||||
Err(e) => println!("Error: {}", e),
|
Err(e) => println!("Error: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -29,20 +25,13 @@ where
|
|||||||
pub async fn create_tables(db: &DbConn) {
|
pub async fn create_tables(db: &DbConn) {
|
||||||
create_table(db, user::Entity).await;
|
create_table(db, user::Entity).await;
|
||||||
create_table(db, permission::Entity).await;
|
create_table(db, permission::Entity).await;
|
||||||
create_table(db, group::Entity).await;
|
|
||||||
create_table(db, user_permission::Entity).await;
|
|
||||||
create_table(db, group_permission::Entity).await;
|
|
||||||
create_table(db, user_group::Entity).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Running Entities manually creates the tables from the entities in their latest incarnation.
|
// Running Entities manually creates the tables from the entities in their latest incarnation.
|
||||||
println!("Connecting to database...");
|
println!("Connecting to database...");
|
||||||
let db: DatabaseConnection =
|
let db: DatabaseConnection = Database::connect("postgresql://miniweb:miniweb@localhost:54321/miniweb").await.unwrap();
|
||||||
Database::connect("postgresql://miniweb:miniweb@localhost:54321/miniweb")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
println!("Creating tables for entities...");
|
println!("Creating tables for entities...");
|
||||||
create_tables(&db).await;
|
create_tables(&db).await;
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,10 @@ pub struct Model {
|
|||||||
pub id: i32,
|
pub id: i32,
|
||||||
#[sea_orm(index = "permission_names")]
|
#[sea_orm(index = "permission_names")]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[sea_orm(index = "permission_codes")]
|
|
||||||
pub codename: String,
|
|
||||||
pub level: i32,
|
pub level: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {}
|
pub enum Relation {}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
@ -1,68 +1,18 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use sea_orm::ActiveValue::{NotSet, Set};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::group::Entity as Group;
|
|
||||||
use super::permission::Entity as Permission;
|
|
||||||
use super::user_group::Relation as UserGroupRelation;
|
|
||||||
use super::user_permission::Relation as UserPermissionRelation;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "users")]
|
#[sea_orm(table_name = "users")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
#[serde(skip_deserializing)]
|
#[serde(skip_deserializing)]
|
||||||
pub id: i64,
|
pub id: i32,
|
||||||
#[sea_orm(index = "user_usernames", unique)]
|
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
#[sea_orm(column_type = "Text")]
|
||||||
|
pub description: Option<String>,
|
||||||
pub first_name: Option<String>,
|
|
||||||
pub last_name: Option<String>,
|
|
||||||
pub email: Option<String>,
|
|
||||||
|
|
||||||
// groups: many-to-many to Group
|
|
||||||
// user_permissions: many-to-many to Permission
|
|
||||||
pub is_staff: bool,
|
|
||||||
pub is_active: bool,
|
|
||||||
pub is_superuser: bool,
|
|
||||||
pub last_login: Option<DateTimeWithTimeZone>,
|
|
||||||
pub date_joined: Option<DateTimeWithTimeZone>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {}
|
pub enum Relation {}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
is_active: Set(true),
|
|
||||||
is_staff: Set(false),
|
|
||||||
is_superuser: Set(false),
|
|
||||||
..<Self as ActiveModelTrait>::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<Group> for Entity {
|
|
||||||
// The final relation is User -> UserGroup -> Group
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
UserGroupRelation::Group.def()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn via() -> Option<RelationDef> {
|
|
||||||
// The original relation is UserGroup -> User,
|
|
||||||
// after `rev` it becomes User -> UserGroup
|
|
||||||
Some(UserGroupRelation::Group.def().rev())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<Permission> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
UserPermissionRelation::Permission.def()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn via() -> Option<RelationDef> {
|
|
||||||
Some(UserPermissionRelation::Permission.def().rev())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
|
|
||||||
pub struct Entity;
|
|
||||||
|
|
||||||
impl EntityName for Entity {
|
|
||||||
fn table_name(&self) -> &str {
|
|
||||||
"user_group"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveModel, DeriveActiveModel)]
|
|
||||||
pub struct Model {
|
|
||||||
pub user_id: i32,
|
|
||||||
pub group_id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
|
||||||
pub enum Column {
|
|
||||||
UserId,
|
|
||||||
GroupId,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
|
|
||||||
pub enum PrimaryKey {
|
|
||||||
UserId,
|
|
||||||
GroupId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimaryKeyTrait for PrimaryKey {
|
|
||||||
type ValueType = (i32, i32);
|
|
||||||
|
|
||||||
fn auto_increment() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColumnTrait for Column {
|
|
||||||
type EntityName = Entity;
|
|
||||||
|
|
||||||
fn def(&self) -> ColumnDef {
|
|
||||||
match self {
|
|
||||||
Self::UserId => ColumnType::Integer.def(),
|
|
||||||
Self::GroupId => ColumnType::Integer.def(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::user::Entity",
|
|
||||||
from = "Column::UserId",
|
|
||||||
to = "super::user::Column::Id"
|
|
||||||
)]
|
|
||||||
User,
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::group::Entity",
|
|
||||||
from = "Column::GroupId",
|
|
||||||
to = "super::group::Column::Id"
|
|
||||||
)]
|
|
||||||
Group,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1,65 +0,0 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
|
|
||||||
pub struct Entity;
|
|
||||||
|
|
||||||
impl EntityName for Entity {
|
|
||||||
fn table_name(&self) -> &str {
|
|
||||||
"user_permission"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveModel, DeriveActiveModel)]
|
|
||||||
pub struct Model {
|
|
||||||
pub user_id: i32,
|
|
||||||
pub permission_id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
|
||||||
pub enum Column {
|
|
||||||
UserId,
|
|
||||||
PermissionId,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
|
|
||||||
pub enum PrimaryKey {
|
|
||||||
UserId,
|
|
||||||
PermissionId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimaryKeyTrait for PrimaryKey {
|
|
||||||
type ValueType = (i32, i32);
|
|
||||||
|
|
||||||
fn auto_increment() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColumnTrait for Column {
|
|
||||||
type EntityName = Entity;
|
|
||||||
|
|
||||||
fn def(&self) -> ColumnDef {
|
|
||||||
match self {
|
|
||||||
Self::UserId => ColumnType::Integer.def(),
|
|
||||||
Self::PermissionId => ColumnType::Integer.def(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::user::Entity",
|
|
||||||
from = "Column::UserId",
|
|
||||||
to = "super::user::Column::Id"
|
|
||||||
)]
|
|
||||||
User,
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::permission::Entity",
|
|
||||||
from = "Column::PermissionId",
|
|
||||||
to = "super::permission::Column::Id"
|
|
||||||
)]
|
|
||||||
Permission,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -11,14 +11,14 @@ path = "src/lib.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.75"
|
anyhow = "1.0.75"
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
minijinja = { version = "2.0.3", features = [
|
minijinja = { version = "1.0.11", features = [
|
||||||
"loader",
|
"loader",
|
||||||
"builtins",
|
"builtins",
|
||||||
"urlencode",
|
"urlencode",
|
||||||
"deserialization",
|
"deserialization",
|
||||||
] }
|
] }
|
||||||
minijinja-autoreload = "2.0.3"
|
minijinja-autoreload = "1.0.8"
|
||||||
pulldown-cmark = "0.11"
|
pulldown-cmark = "0.10"
|
||||||
serde = { version = "1.0.188", features = ["derive"] }
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
tokio = { version = "1.32.0", features = ["full"] }
|
tokio = { version = "1.32.0", features = ["full"] }
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
@ -26,4 +26,3 @@ serde_json = "1.0.108"
|
|||||||
slug = "0.1.5"
|
slug = "0.1.5"
|
||||||
async-trait = "0.1.77"
|
async-trait = "0.1.77"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
thiserror = "1.0.61"
|
|
||||||
|
482
rear/src/admin/domain.rs
Normal file
482
rear/src/admin/domain.rs
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
pub use config::AdminModelConfig;
|
||||||
|
pub use dto::AdminApp;
|
||||||
|
pub use dto::AdminModel;
|
||||||
|
pub use repository::{
|
||||||
|
AdminRepository, DynAdminRepository, RepoInfo, RepositoryContext, RepositoryInfo,
|
||||||
|
RepositoryItem, RepositoryList, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod auth {
|
||||||
|
|
||||||
|
struct AdminUser {}
|
||||||
|
|
||||||
|
struct AdminRole {}
|
||||||
|
|
||||||
|
struct AdminGroup {}
|
||||||
|
|
||||||
|
struct AdminActionLog {}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod config {
|
||||||
|
// user uses this configuration object to register another model.
|
||||||
|
pub struct AdminModelConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub app_key: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod dto {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct AdminModel {
|
||||||
|
pub key: String,
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
pub admin_url: String,
|
||||||
|
pub view_only: bool,
|
||||||
|
|
||||||
|
pub add_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct AdminApp {
|
||||||
|
pub key: String,
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
pub app_url: String,
|
||||||
|
pub models: Vec<AdminModel>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod repository {
|
||||||
|
use super::dto::AdminModel;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use serde::{Serialize, Serializer};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::any::Any;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::vec::IntoIter;
|
||||||
|
|
||||||
|
pub type RepositoryContext = AdminModel;
|
||||||
|
|
||||||
|
impl RepositoryContext {
|
||||||
|
pub fn get_default_detail_url(&self, key: &str) -> Option<String> {
|
||||||
|
Some(format!("{}/detail/{}", self.admin_url, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_change_url(&self, key: &str) -> Option<String> {
|
||||||
|
Some(format!("{}/change/{}", self.admin_url, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem {
|
||||||
|
RepositoryItem {
|
||||||
|
detail_url: self.get_default_detail_url(key),
|
||||||
|
change_url: self.get_default_change_url(key),
|
||||||
|
fields: fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is a static configuration object.
|
||||||
|
/// It might be changed in the future to have a dynamic counterpart.
|
||||||
|
///
|
||||||
|
/// ## Example:
|
||||||
|
/// Creating a simple required and readonly text input field with a label:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let my_field = Field::widget("/admin/widgets/input_text.jinja")
|
||||||
|
/// .labelled("Username")
|
||||||
|
/// .required()
|
||||||
|
/// .readonly();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Serialize, Clone, Copy)]
|
||||||
|
pub struct Widget {
|
||||||
|
pub widget: &'static str,
|
||||||
|
pub label: Option<&'static str>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub field_type: &'static str,
|
||||||
|
pub required: bool,
|
||||||
|
pub readonly: bool,
|
||||||
|
pub options: &'static [(&'static str, &'static str)],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget {
|
||||||
|
pub fn widget(widget: &'static str) -> Self {
|
||||||
|
Widget {
|
||||||
|
widget: widget,
|
||||||
|
label: None,
|
||||||
|
field_type: "text",
|
||||||
|
required: false,
|
||||||
|
readonly: false,
|
||||||
|
options: &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self::widget("/admin/widgets/input_text.jinja")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn textarea() -> Self {
|
||||||
|
Self::widget("/admin/widgets/input_textarea.jinja")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required(mut self) -> Self {
|
||||||
|
self.required = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readonly(mut self) -> Self {
|
||||||
|
self.readonly = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn labeled(mut self, label: &'static str) -> Self {
|
||||||
|
self.label = Some(label);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_password(mut self) -> Self {
|
||||||
|
self.field_type = "password";
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_hidden(mut self) -> Self {
|
||||||
|
self.field_type = "hidden";
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn options(mut self, options: &'static [(&'static str, &'static str)]) -> Self {
|
||||||
|
self.options = options;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Field {
|
||||||
|
widget: String,
|
||||||
|
label: Option<String>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
field_type: String,
|
||||||
|
readonly: bool,
|
||||||
|
required: bool,
|
||||||
|
options: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Widget> for Field {
|
||||||
|
fn from(value: Widget) -> Self {
|
||||||
|
Field {
|
||||||
|
widget: value.widget.to_string(),
|
||||||
|
label: value.label.map(|s| s.to_string()),
|
||||||
|
field_type: value.field_type.to_string(),
|
||||||
|
readonly: value.readonly,
|
||||||
|
required: value.required,
|
||||||
|
options: value
|
||||||
|
.options
|
||||||
|
.iter()
|
||||||
|
.map(|(key, val)| (key.to_string(), serde_json::json!(val)))
|
||||||
|
.collect::<serde_json::Map<String, Value>>()
|
||||||
|
.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RepositoryItem {
|
||||||
|
pub fields: Value,
|
||||||
|
pub detail_url: Option<String>,
|
||||||
|
pub change_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RepositoryList {
|
||||||
|
Empty,
|
||||||
|
List {
|
||||||
|
values: Vec<RepositoryItem>,
|
||||||
|
},
|
||||||
|
Page {
|
||||||
|
values: Vec<RepositoryItem>,
|
||||||
|
offset: usize,
|
||||||
|
total: usize,
|
||||||
|
},
|
||||||
|
Stream {
|
||||||
|
values: Vec<RepositoryItem>,
|
||||||
|
next_index: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for RepositoryList {
|
||||||
|
type Item = RepositoryItem;
|
||||||
|
type IntoIter = IntoIter<Self::Item>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
match self {
|
||||||
|
RepositoryList::Empty => vec![].into_iter(),
|
||||||
|
RepositoryList::List { values } => values.into_iter(),
|
||||||
|
RepositoryList::Page { values, .. } => values.into_iter(),
|
||||||
|
RepositoryList::Stream { values, .. } => values.into_iter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for RepositoryList {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
RepositoryList::Empty => serializer.serialize_unit(),
|
||||||
|
RepositoryList::List { values }
|
||||||
|
| RepositoryList::Page { values, .. }
|
||||||
|
| RepositoryList::Stream { values, .. } => values.serialize(serializer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Static initializer for RepositoryInfo.
|
||||||
|
pub struct RepoInfo {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub lookup_key: &'static str,
|
||||||
|
pub display_list: &'static [&'static str],
|
||||||
|
pub fields: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepoInfo {
|
||||||
|
pub fn build(self) -> RepositoryInfo {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RepositoryInfo {
|
||||||
|
name: String,
|
||||||
|
lookup_key: String,
|
||||||
|
display_list: Vec<String>,
|
||||||
|
fields: Vec<(String, Field)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepositoryInfo {
|
||||||
|
pub fn new(name: &str, lookup_key: &str) -> Self {
|
||||||
|
RepositoryInfo {
|
||||||
|
name: name.to_owned(),
|
||||||
|
lookup_key: lookup_key.to_owned(),
|
||||||
|
display_list: vec![],
|
||||||
|
fields: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// self mutating builder pattern
|
||||||
|
pub fn display_list(mut self, display_list: &[&str]) -> Self {
|
||||||
|
self.display_list = display_list.iter().map(|&e| e.to_string()).collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_widget<T: Into<Field>>(mut self, name: &str, item: T) -> Self {
|
||||||
|
let field = item.into(); // Convert the input into a Field
|
||||||
|
|
||||||
|
// Find the index of the existing entry with the same name, if it exists
|
||||||
|
let pos = self
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.position(|(existing_name, _)| existing_name == name);
|
||||||
|
|
||||||
|
match pos {
|
||||||
|
Some(index) => {
|
||||||
|
self.fields[index].1 = field;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.fields.push((name.to_owned(), field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RepoInfo> for RepositoryInfo {
|
||||||
|
fn from(repo_info: RepoInfo) -> Self {
|
||||||
|
RepositoryInfo {
|
||||||
|
name: repo_info.name.to_string(),
|
||||||
|
lookup_key: repo_info.lookup_key.to_string(),
|
||||||
|
display_list: repo_info
|
||||||
|
.display_list
|
||||||
|
.iter()
|
||||||
|
.map(|&s| s.to_string())
|
||||||
|
.collect(),
|
||||||
|
fields: repo_info
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.map(|x| (x.to_string(), Field::from(Widget::default())))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PrimaryKeyType: Any + Debug + Send + Sync {
|
||||||
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimaryKeyType for i64 {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimaryKeyType for String {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AdminRepository: Send + Sync {
|
||||||
|
type Key: PrimaryKeyType;
|
||||||
|
|
||||||
|
fn key_from_string(&self, s: String) -> Option<Self::Key>;
|
||||||
|
|
||||||
|
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
|
||||||
|
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
|
||||||
|
async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem>;
|
||||||
|
async fn create(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &Self::Key,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn replace(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &Self::Key,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn delete(&mut self, context: &RepositoryContext, id: &Self::Key) -> Option<Value>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait DynAdminRepository: Send + Sync {
|
||||||
|
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>>;
|
||||||
|
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
|
||||||
|
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn create(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn replace(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem>;
|
||||||
|
async fn delete(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
) -> Option<Value>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AdminRepositoryWrapper<T: AdminRepository> {
|
||||||
|
inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AdminRepository> AdminRepositoryWrapper<T> {
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_from_string(&self, s: String) -> Option<<T as AdminRepository>::Key> {
|
||||||
|
self.inner.key_from_string(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: AdminRepository> DynAdminRepository for AdminRepositoryWrapper<T> {
|
||||||
|
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>> {
|
||||||
|
if let Some(key) = self.inner.key_from_string(s) {
|
||||||
|
Some(Box::new(key))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo {
|
||||||
|
self.inner.info(context).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list(&self, context: &RepositoryContext) -> RepositoryList {
|
||||||
|
self.inner.list(context).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
||||||
|
self.inner.get(context, key).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
self.inner.create(context, data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
||||||
|
self.inner.update(context, key, data).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn replace(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
||||||
|
self.inner.replace(context, key, data).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(
|
||||||
|
&mut self,
|
||||||
|
context: &RepositoryContext,
|
||||||
|
id: &dyn PrimaryKeyType,
|
||||||
|
) -> Option<Value> {
|
||||||
|
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
||||||
|
self.inner.delete(context, key).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,34 +1,27 @@
|
|||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, Router};
|
||||||
|
|
||||||
mod constants;
|
pub mod domain;
|
||||||
mod context;
|
|
||||||
pub mod prelude;
|
|
||||||
mod registry;
|
|
||||||
pub mod repository;
|
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod views;
|
pub mod views;
|
||||||
pub mod widgets;
|
|
||||||
|
|
||||||
use state::DepotFn;
|
pub fn routes<S: state::AdminState + Clone + Send + Sync + 'static>() -> Router<S> {
|
||||||
|
|
||||||
pub fn routes<S: DepotFn>() -> Router<S> {
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(views::index::<S>).post(views::index_action::<S>))
|
.route("/", get(views::index::<S>).post(views::index_action::<S>))
|
||||||
.route("/:section", get(views::list_section::<S>))
|
.route("/app/:app", get(views::list_app::<S>))
|
||||||
.route(
|
.route(
|
||||||
"/:section/model/:model",
|
"/app/:app/model/:model",
|
||||||
get(views::list_item_collection::<S>),
|
get(views::list_item_collection::<S>),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/:section/model/:model/add",
|
"/app/:app/model/:model/add",
|
||||||
get(views::new_item::<S>).post(views::create_item::<S>),
|
get(views::new_item::<S>).post(views::create_item::<S>),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/:section/model/:model/change/:id",
|
"/app/:app/model/:model/change/:id",
|
||||||
get(views::change_item::<S>).patch(views::update_item::<S>),
|
get(views::change_item::<S>).patch(views::update_item::<S>),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/:section/model/:model/detail/:id",
|
"/app/:app/model/:model/detail/:id",
|
||||||
get(views::view_item_details::<S>),
|
get(views::view_item_details::<S>),
|
||||||
)
|
)
|
||||||
}
|
}
|
174
rear/src/admin/state.rs
Normal file
174
rear/src/admin/state.rs
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use super::domain::repository::AdminRepositoryWrapper;
|
||||||
|
use super::domain::{AdminApp, AdminModel, AdminModelConfig, AdminRepository, DynAdminRepository};
|
||||||
|
use crate::service::templates::Templates;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub trait AdminState {
|
||||||
|
fn get_templates(&self) -> &Templates;
|
||||||
|
fn get_registry(&self) -> SharedAdminRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SharedAdminRegistry = Arc<AdminRegistry>;
|
||||||
|
|
||||||
|
// main registry.
|
||||||
|
pub struct AdminRegistry {
|
||||||
|
base_path: String,
|
||||||
|
apps: HashMap<String, internal::AdminApp>,
|
||||||
|
models: HashMap<String, internal::AdminModel>,
|
||||||
|
repositories: HashMap<String, Arc<Mutex<dyn DynAdminRepository>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdminRegistry {
|
||||||
|
pub fn new(base_path: &str) -> Self {
|
||||||
|
AdminRegistry {
|
||||||
|
base_path: base_path.to_owned(),
|
||||||
|
apps: HashMap::new(),
|
||||||
|
models: HashMap::new(),
|
||||||
|
repositories: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_apps(&self) -> Vec<AdminApp> {
|
||||||
|
self.apps
|
||||||
|
.iter()
|
||||||
|
.map(|(key, node)| self.get_app(key, node))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_app(&self, key: &str, node: &internal::AdminApp) -> AdminApp {
|
||||||
|
let my_models = self.get_models(key);
|
||||||
|
AdminApp {
|
||||||
|
name: key.to_owned(),
|
||||||
|
key: node.name.to_owned(),
|
||||||
|
app_url: format!("/{}/app/{}", self.base_path, key.to_owned()),
|
||||||
|
models: my_models,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_app(&mut self, name: &str) -> String {
|
||||||
|
let key = self.get_key(name);
|
||||||
|
self.apps.insert(
|
||||||
|
key.to_owned(),
|
||||||
|
internal::AdminApp {
|
||||||
|
key: key.to_owned(),
|
||||||
|
name: name.to_owned(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_key(&self, name: &str) -> String {
|
||||||
|
slug::slugify(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn model_from_internal(&self, internal_model: &internal::AdminModel) -> AdminModel {
|
||||||
|
let admin_url = format!(
|
||||||
|
"/{}/app/{}/model/{}",
|
||||||
|
self.base_path, internal_model.app_key, internal_model.model_key
|
||||||
|
);
|
||||||
|
AdminModel {
|
||||||
|
key: internal_model.model_key.clone(),
|
||||||
|
name: internal_model.name.clone(),
|
||||||
|
view_only: false,
|
||||||
|
add_url: Some(format!("{}/add", admin_url)),
|
||||||
|
admin_url: admin_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_models(&self, app_key: &str) -> Vec<AdminModel> {
|
||||||
|
self.models
|
||||||
|
.iter()
|
||||||
|
.filter(|(key, _)| key.starts_with(&format!("{}.", app_key)))
|
||||||
|
.map(|(_, model)| self.model_from_internal(model))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_model(&self, app_key: &str, model_key: &str) -> Option<AdminModel> {
|
||||||
|
let full_model_key = format!("{}.{}", app_key, model_key);
|
||||||
|
let internal_model = self.models.get(&full_model_key)?;
|
||||||
|
|
||||||
|
// unfinished: we need to think about model_key vs. model_id vs. entry_id, as "name" is ambiguous.
|
||||||
|
Some(self.model_from_internal(internal_model))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_model_config(&mut self, model: AdminModelConfig) -> Result<String, String> {
|
||||||
|
let local_config = internal::AdminModel::from(model);
|
||||||
|
if local_config.model_key.is_empty() {
|
||||||
|
return Err("No model name".to_owned());
|
||||||
|
}
|
||||||
|
let local_config_name = format!("{}.{}", local_config.app_key, local_config.model_key);
|
||||||
|
if self.models.contains_key(&local_config_name) {
|
||||||
|
return Err(format!("Model {} already exists", local_config_name));
|
||||||
|
}
|
||||||
|
let full_model_key = local_config_name.clone();
|
||||||
|
self.models.insert(local_config_name, local_config);
|
||||||
|
Ok(full_model_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_model<R: AdminRepository + 'static>(
|
||||||
|
&mut self,
|
||||||
|
model: AdminModelConfig,
|
||||||
|
repository: R,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let model_key = self.register_model_config(model)?;
|
||||||
|
let repository = AdminRepositoryWrapper::new(repository);
|
||||||
|
self.repositories
|
||||||
|
.insert(model_key, Arc::new(Mutex::new(repository)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_repository(
|
||||||
|
&self,
|
||||||
|
app_key: &str,
|
||||||
|
model_key: &str,
|
||||||
|
) -> Result<Arc<Mutex<dyn DynAdminRepository>>, String> {
|
||||||
|
let full_model_key = format!("{}.{}", app_key, model_key);
|
||||||
|
if let Some(repo) = self.repositories.get(&full_model_key) {
|
||||||
|
// Clone the Arc to return a reference to the repository
|
||||||
|
return Ok(Arc::clone(repo));
|
||||||
|
} else {
|
||||||
|
return Err("Couldn't find repository".to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod internal {
|
||||||
|
// how the registry saves data internally.
|
||||||
|
|
||||||
|
use super::super::domain::AdminModelConfig;
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AdminApp {
|
||||||
|
pub key: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AdminModel {
|
||||||
|
pub app_key: String,
|
||||||
|
pub model_key: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AdminModelConfig> for AdminModel {
|
||||||
|
fn from(value: AdminModelConfig) -> Self {
|
||||||
|
AdminModel {
|
||||||
|
app_key: value.app_key,
|
||||||
|
model_key: slug::slugify(value.name.clone()),
|
||||||
|
name: value.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(&str, &str)> for AdminModel {
|
||||||
|
fn from(value: (&str, &str)) -> Self {
|
||||||
|
AdminModel {
|
||||||
|
app_key: value.0.to_owned(),
|
||||||
|
model_key: slug::slugify(value.1.to_owned()),
|
||||||
|
name: value.1.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
349
rear/src/admin/views.rs
Normal file
349
rear/src/admin/views.rs
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
use axum::extract::Path;
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use axum::Form;
|
||||||
|
use axum::{extract::State, response::IntoResponse};
|
||||||
|
use log::info;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::admin::domain::{AdminApp, AdminModel};
|
||||||
|
use crate::admin::state::AdminState;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::domain::{RepositoryInfo, RepositoryItem, RepositoryList};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct AdminRequest {
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct AdminContext {
|
||||||
|
pub base: Option<String>,
|
||||||
|
pub language_code: Option<String>,
|
||||||
|
pub language_bidi: Option<bool>,
|
||||||
|
pub user: Option<String>, // Todo: user type
|
||||||
|
pub admin_url: String,
|
||||||
|
pub site_url: Option<String>,
|
||||||
|
pub docsroot: Option<String>,
|
||||||
|
pub messages: Vec<String>, // Todo: message type
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub subtitle: Option<String>,
|
||||||
|
pub content: String,
|
||||||
|
|
||||||
|
pub request: AdminRequest,
|
||||||
|
pub available_apps: Vec<AdminApp>,
|
||||||
|
pub item_model: Option<AdminModel>,
|
||||||
|
pub item_info: Option<RepositoryInfo>,
|
||||||
|
pub item_list: RepositoryList,
|
||||||
|
pub item: Option<RepositoryItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AdminContext {
|
||||||
|
fn default() -> Self {
|
||||||
|
AdminContext {
|
||||||
|
base: None, // TODO: what is this used for?
|
||||||
|
language_code: Some("en-us".to_string()), // Default language code
|
||||||
|
language_bidi: Some(false), // Default language bidi
|
||||||
|
user: None, //UserType::default(), // Assuming UserType has a Default impl
|
||||||
|
admin_url: "/admin".to_owned(),
|
||||||
|
site_url: None,
|
||||||
|
docsroot: None,
|
||||||
|
messages: Vec::new(), // Empty vector for messages
|
||||||
|
title: None,
|
||||||
|
subtitle: None,
|
||||||
|
content: String::new(), // Empty string for content
|
||||||
|
available_apps: Vec::new(),
|
||||||
|
request: AdminRequest {
|
||||||
|
path: "".to_owned(),
|
||||||
|
},
|
||||||
|
item_model: None,
|
||||||
|
item_info: None,
|
||||||
|
item_list: RepositoryList::Empty,
|
||||||
|
item: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn base_template(headers: &HeaderMap) -> Option<String> {
|
||||||
|
let hx_request = headers.get("HX-Request").is_some();
|
||||||
|
if hx_request {
|
||||||
|
Some("admin/base_hx.jinja".to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn index<S: AdminState + Clone + Send + Sync + 'static>(
|
||||||
|
admin: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = admin.get_templates();
|
||||||
|
let registry = admin.get_registry();
|
||||||
|
templates.render_html(
|
||||||
|
"admin/index.html",
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index Action is POST to the index site. We can anchor some general business code here.
|
||||||
|
pub async fn index_action<S: AdminState + Clone + Send + Sync + 'static>(
|
||||||
|
admin: State<S>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
"There is your answer!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_app<S: AdminState + Clone + Send + Sync + 'static>(
|
||||||
|
admin: State<S>,
|
||||||
|
Path(app_key): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = admin.get_templates();
|
||||||
|
templates.render_html("admin/app_list.jinja", ())
|
||||||
|
}
|
||||||
|
|
||||||
|
// List Items renders the entire list item page.
|
||||||
|
pub async fn list_item_collection<S: AdminState + Clone + Send + Sync + 'static>(
|
||||||
|
admin: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((app_key, model_key)): Path<(String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
info!("list_item_collection {} for model {}", app_key, model_key);
|
||||||
|
let templates = admin.get_templates();
|
||||||
|
let registry = admin.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let admin_model = registry
|
||||||
|
.get_model(&app_key, &model_key)
|
||||||
|
.expect("Admin Model not found?"); // we will need a proper error route; so something that implements IntoResponse and can be substituted in the unwraps and expects.
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
item_info: Some(repo.info(&admin_model).await),
|
||||||
|
item_list: repo.list(&admin_model).await,
|
||||||
|
item_model: Some(admin_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("admin/items/item_list.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items Action is a POST to an item list. By default these are actions, that work on a list of items as input.
|
||||||
|
pub async fn item_collection_action<S: AdminState + Clone + Send + Sync + 'static>(
|
||||||
|
admin: State<S>,
|
||||||
|
Path((app_key, model_key)): Path<(String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
"There is your answer!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item Details shows one single dataset.
|
||||||
|
pub async fn view_item_details<S: AdminState + Clone + Send + Sync + 'static>(
|
||||||
|
admin: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((app_key, model_key, id)): Path<(String, String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = admin.get_templates();
|
||||||
|
let registry = admin.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let admin_model = registry
|
||||||
|
.get_model(&app_key, &model_key)
|
||||||
|
.expect("Admin Model not found?");
|
||||||
|
if let Some(key) = repo.key_from_string(id) {
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
item_info: Some(repo.info(&admin_model).await),
|
||||||
|
item_list: repo.list(&admin_model).await,
|
||||||
|
item: repo.get(&admin_model, key.as_ref()).await,
|
||||||
|
item_model: Some(admin_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("admin/items/item_detail.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_item<S: AdminState + Clone + Send + Sync + 'static>(
|
||||||
|
admin: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((app_key, model_key)): Path<(String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = admin.get_templates();
|
||||||
|
let registry = admin.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let admin_model = registry
|
||||||
|
.get_model(&app_key, &model_key)
|
||||||
|
.expect("Admin Model not found?");
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
item_info: Some(repo.info(&admin_model).await),
|
||||||
|
item_list: repo.list(&admin_model).await,
|
||||||
|
item_model: Some(admin_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("admin/items/item_create.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_item<S: AdminState + Clone + Send + Sync + 'static>(
|
||||||
|
admin: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((app_key, model_key)): Path<(String, String)>,
|
||||||
|
Form(form): Form<Value>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = admin.get_templates();
|
||||||
|
let registry = admin.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
||||||
|
let mut repo = repo.lock().await;
|
||||||
|
let admin_model = registry
|
||||||
|
.get_model(&app_key, &model_key)
|
||||||
|
.expect("Admin Model not found?");
|
||||||
|
|
||||||
|
// create our item.
|
||||||
|
let result = repo.create(&admin_model, form).await;
|
||||||
|
|
||||||
|
// TODO: refactor run over these views, way too much repetition.
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
item_info: Some(repo.info(&admin_model).await),
|
||||||
|
item_list: repo.list(&admin_model).await,
|
||||||
|
item_model: Some(admin_model),
|
||||||
|
item: result,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("admin/items/item_create.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change is the GET version.
|
||||||
|
pub async fn change_item<S: AdminState + Clone + Send + Sync + 'static>(
|
||||||
|
admin: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((app_key, model_key, id)): Path<(String, String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = admin.get_templates();
|
||||||
|
let registry = admin.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
||||||
|
let repo = repo.lock().await;
|
||||||
|
let admin_model = registry
|
||||||
|
.get_model(&app_key, &model_key)
|
||||||
|
.expect("Admin Model not found?");
|
||||||
|
if let Some(key) = repo.key_from_string(id) {
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
item_info: Some(repo.info(&admin_model).await),
|
||||||
|
item_list: repo.list(&admin_model).await,
|
||||||
|
item: repo.get(&admin_model, key.as_ref()).await,
|
||||||
|
item_model: Some(admin_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("admin/items/item_change.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_item<S: AdminState + Clone + Send + Sync + 'static>(
|
||||||
|
admin: State<S>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((app_key, model_key, id)): Path<(String, String, String)>,
|
||||||
|
Form(form): Form<Value>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let templates = admin.get_templates();
|
||||||
|
let registry = admin.get_registry();
|
||||||
|
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
|
||||||
|
let mut repo = repo.lock().await;
|
||||||
|
let admin_model = registry
|
||||||
|
.get_model(&app_key, &model_key)
|
||||||
|
.expect("Admin Model not found?");
|
||||||
|
if let Some(key) = repo.key_from_string(id) {
|
||||||
|
let result = repo.update(&admin_model, key.as_ref(), form).await;
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
item_info: Some(repo.info(&admin_model).await),
|
||||||
|
item_list: repo.list(&admin_model).await,
|
||||||
|
item: result,
|
||||||
|
item_model: Some(admin_model),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdminContext {
|
||||||
|
base: base_template(&headers),
|
||||||
|
available_apps: registry.get_apps(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
templates.render_html("admin/items/item_change.jinja", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item Action allows running an action on one single dataset.
|
||||||
|
pub async fn item_action<S: AdminState + Clone + Send + Sync + 'static>(
|
||||||
|
admin: State<S>,
|
||||||
|
Path((app_key, model_key, model_id)): Path<(String, String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
"There is your answer!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn debug_view<S: AdminState + Clone + Send + Sync + 'static>(
|
||||||
|
admin: State<S>,
|
||||||
|
Path(data): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
println!("debug: {}", data);
|
||||||
|
"Debug!".to_owned()
|
||||||
|
}
|
1
rear/src/auth/mod.rs
Normal file
1
rear/src/auth/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod models;
|
8
rear/src/auth/models.rs
Normal file
8
rear/src/auth/models.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
pub struct User {
|
||||||
|
pub id: i32,
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NewUser<'a> {
|
||||||
|
pub username: &'a str,
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
pub(crate) struct TemplateFiles {
|
|
||||||
base_hx: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub static TPL: TemplateFiles = TemplateFiles { base_hx: "" };
|
|
@ -1,78 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::repository::{RepositoryInfo, RepositoryItem, RepositoryList};
|
|
||||||
|
|
||||||
// representation of a Model in the template.
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct DepotModel {
|
|
||||||
pub key: String,
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
pub model_url: String,
|
|
||||||
pub view_only: bool,
|
|
||||||
|
|
||||||
pub add_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// representation of a Section in the template.
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct DepotSection {
|
|
||||||
pub key: String,
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
pub section_url: String,
|
|
||||||
pub models: Vec<DepotModel>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct DepotRequest {
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct DepotContext {
|
|
||||||
pub base: Option<String>,
|
|
||||||
pub language_code: Option<String>,
|
|
||||||
pub language_bidi: Option<bool>,
|
|
||||||
pub user: Option<String>, // Todo: user type
|
|
||||||
pub depot_url: String,
|
|
||||||
pub site_url: Option<String>,
|
|
||||||
pub docsroot: Option<String>,
|
|
||||||
pub messages: Vec<String>, // Todo: message type
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub subtitle: Option<String>,
|
|
||||||
pub content: String,
|
|
||||||
|
|
||||||
pub request: DepotRequest,
|
|
||||||
pub sections: Vec<DepotSection>,
|
|
||||||
pub item_model: Option<DepotModel>,
|
|
||||||
pub item_info: Option<RepositoryInfo>,
|
|
||||||
pub item_list: RepositoryList,
|
|
||||||
pub item: Option<RepositoryItem>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DepotContext {
|
|
||||||
fn default() -> Self {
|
|
||||||
DepotContext {
|
|
||||||
base: None, // TODO: what is this used for?
|
|
||||||
language_code: Some("en-us".to_string()), // Default language code
|
|
||||||
language_bidi: Some(false), // Default language bidi
|
|
||||||
user: None, //UserType::default(), // Assuming UserType has a Default impl
|
|
||||||
depot_url: "/depot".to_owned(),
|
|
||||||
site_url: None,
|
|
||||||
docsroot: None,
|
|
||||||
messages: Vec::new(), // Empty vector for messages
|
|
||||||
title: None,
|
|
||||||
subtitle: None,
|
|
||||||
content: String::new(), // Empty string for content
|
|
||||||
sections: Vec::new(),
|
|
||||||
request: DepotRequest {
|
|
||||||
path: "".to_owned(),
|
|
||||||
},
|
|
||||||
item_model: None,
|
|
||||||
item_info: None,
|
|
||||||
item_list: RepositoryList::Empty,
|
|
||||||
item: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
UPDATE:
|
|
||||||
- The Item is edited successfully: Success Message
|
|
||||||
- The Item could not be saved, it needs adjustment: Form Errors, Warning Message?
|
|
||||||
- The Item could not be saved, there was an Error of some sort: Error Message
|
|
||||||
|
|
||||||
Widget -builds-> Field
|
|
||||||
|
|
||||||
|
|
||||||
Validation: https://github.com/tokio-rs/axum/blob/main/examples/validator/src/main.rs
|
|
||||||
AvoRED PRoject seems similar: https://github.com/avored/avored-rust-cms
|
|
@ -1,7 +0,0 @@
|
|||||||
pub use super::context::{DepotContext, DepotModel};
|
|
||||||
pub use super::registry::DepotRegistry;
|
|
||||||
pub use super::repository::{
|
|
||||||
DepotModelConfig, DepotRepository, RepoInfo, RepositoryContext, RepositoryError,
|
|
||||||
RepositoryInfo, RepositoryItem, RepositoryList, RepositoryResponse, RepositoryResult,
|
|
||||||
};
|
|
||||||
pub use super::widgets::Widget;
|
|
@ -1,164 +0,0 @@
|
|||||||
use std::{collections::HashMap, sync::Arc};
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
context::{DepotModel, DepotSection},
|
|
||||||
repository::{DepotModelConfig, DepotRepository, DepotRepositoryWrapper, DynDepotRepository},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct DepotRegistry {
|
|
||||||
base_path: String,
|
|
||||||
sections: HashMap<String, internal::DepotSectionInfo>,
|
|
||||||
models: HashMap<String, internal::DepotModelInfo>,
|
|
||||||
repositories: HashMap<String, Arc<Mutex<dyn DynDepotRepository>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DepotRegistry {
|
|
||||||
pub fn new(base_path: &str) -> Self {
|
|
||||||
DepotRegistry {
|
|
||||||
base_path: base_path.to_owned(),
|
|
||||||
sections: HashMap::new(),
|
|
||||||
models: HashMap::new(),
|
|
||||||
repositories: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_sections(&self) -> Vec<DepotSection> {
|
|
||||||
self.sections
|
|
||||||
.iter()
|
|
||||||
.map(|(key, section_info)| self.get_section(key, section_info))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_section(&self, key: &str, section_info: &internal::DepotSectionInfo) -> DepotSection {
|
|
||||||
let my_models = self.get_models(key);
|
|
||||||
DepotSection {
|
|
||||||
key: key.to_owned(),
|
|
||||||
name: section_info.name.to_owned(),
|
|
||||||
section_url: format!("/{}/{}", self.base_path, key.to_owned()),
|
|
||||||
models: my_models,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register_section(&mut self, name: &str) -> String {
|
|
||||||
let key = self.get_key(name);
|
|
||||||
self.sections.insert(
|
|
||||||
key.to_owned(),
|
|
||||||
internal::DepotSectionInfo {
|
|
||||||
key: key.to_owned(),
|
|
||||||
name: name.to_owned(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
key
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_key(&self, name: &str) -> String {
|
|
||||||
slug::slugify(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn model_from_model_info(&self, model_info: &internal::DepotModelInfo) -> DepotModel {
|
|
||||||
let model_url = format!(
|
|
||||||
"/{}/{}/model/{}",
|
|
||||||
self.base_path, model_info.section_key, model_info.model_key
|
|
||||||
);
|
|
||||||
DepotModel {
|
|
||||||
key: model_info.model_key.clone(),
|
|
||||||
name: model_info.name.clone(),
|
|
||||||
view_only: false,
|
|
||||||
add_url: Some(format!("{}/add", model_url)),
|
|
||||||
model_url: model_url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_models(&self, section_key: &str) -> Vec<DepotModel> {
|
|
||||||
self.models
|
|
||||||
.iter()
|
|
||||||
.filter(|(key, _)| key.starts_with(&format!("{}.", section_key)))
|
|
||||||
.map(|(_, model)| self.model_from_model_info(model))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_model(&self, section_key: &str, model_key: &str) -> Option<DepotModel> {
|
|
||||||
let full_model_key = format!("{}.{}", section_key, model_key);
|
|
||||||
let internal_model = self.models.get(&full_model_key)?;
|
|
||||||
Some(self.model_from_model_info(internal_model))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_model_config(&mut self, model: DepotModelConfig) -> Result<String, String> {
|
|
||||||
let local_config = internal::DepotModelInfo::from(model);
|
|
||||||
if local_config.model_key.is_empty() {
|
|
||||||
return Err("No model name".to_owned());
|
|
||||||
}
|
|
||||||
let local_config_name = format!("{}.{}", local_config.section_key, local_config.model_key);
|
|
||||||
if self.models.contains_key(&local_config_name) {
|
|
||||||
return Err(format!("Model {} already exists", local_config_name));
|
|
||||||
}
|
|
||||||
let full_model_key = local_config_name.clone();
|
|
||||||
self.models.insert(local_config_name, local_config);
|
|
||||||
Ok(full_model_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register_model<R: DepotRepository + 'static>(
|
|
||||||
&mut self,
|
|
||||||
model: DepotModelConfig,
|
|
||||||
repository: R,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let model_key = self.register_model_config(model)?;
|
|
||||||
let repository = DepotRepositoryWrapper::new(repository);
|
|
||||||
self.repositories
|
|
||||||
.insert(model_key, Arc::new(Mutex::new(repository)));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_repository(
|
|
||||||
&self,
|
|
||||||
section_key: &str,
|
|
||||||
model_key: &str,
|
|
||||||
) -> Result<Arc<Mutex<dyn DynDepotRepository>>, String> {
|
|
||||||
let full_model_key = format!("{}.{}", section_key, model_key);
|
|
||||||
if let Some(repo) = self.repositories.get(&full_model_key) {
|
|
||||||
// Clone the Arc to return a reference to the repository
|
|
||||||
return Ok(Arc::clone(repo));
|
|
||||||
} else {
|
|
||||||
return Err("Couldn't find repository".to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod internal {
|
|
||||||
// how the registry saves data internally.
|
|
||||||
use super::super::repository::DepotModelConfig;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(super) struct DepotSectionInfo {
|
|
||||||
pub key: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(super) struct DepotModelInfo {
|
|
||||||
pub section_key: String,
|
|
||||||
pub model_key: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DepotModelConfig> for DepotModelInfo {
|
|
||||||
fn from(value: DepotModelConfig) -> Self {
|
|
||||||
DepotModelInfo {
|
|
||||||
section_key: value.section_key,
|
|
||||||
model_key: slug::slugify(value.name.clone()),
|
|
||||||
name: value.name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(&str, &str)> for DepotModelInfo {
|
|
||||||
fn from(value: (&str, &str)) -> Self {
|
|
||||||
DepotModelInfo {
|
|
||||||
section_key: value.0.to_owned(),
|
|
||||||
model_key: slug::slugify(value.1.to_owned()),
|
|
||||||
name: value.1.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,391 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use serde::{Serialize, Serializer};
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::any::Any;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::vec::IntoIter;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use super::context::DepotModel;
|
|
||||||
use super::widgets::Widget;
|
|
||||||
|
|
||||||
// user uses this configuration object to register another model.
|
|
||||||
pub struct DepotModelConfig {
|
|
||||||
pub name: String,
|
|
||||||
pub section_key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(&str, &str)> for DepotModelConfig {
|
|
||||||
fn from(value: (&str, &str)) -> Self {
|
|
||||||
DepotModelConfig {
|
|
||||||
section_key: value.0.to_owned(),
|
|
||||||
name: value.1.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// May become it's own structure.
|
|
||||||
pub type RepositoryContext = DepotModel;
|
|
||||||
|
|
||||||
impl RepositoryContext {
|
|
||||||
pub fn get_default_detail_url(&self, key: &str) -> Option<String> {
|
|
||||||
Some(format!("{}/detail/{}", self.model_url, key))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_default_change_url(&self, key: &str) -> Option<String> {
|
|
||||||
Some(format!("{}/change/{}", self.model_url, key))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem {
|
|
||||||
RepositoryItem {
|
|
||||||
detail_url: self.get_default_detail_url(key),
|
|
||||||
change_url: self.get_default_change_url(key),
|
|
||||||
fields: fields,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct Field {
|
|
||||||
widget: String,
|
|
||||||
label: Option<String>,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
field_type: String,
|
|
||||||
readonly: bool,
|
|
||||||
required: bool,
|
|
||||||
options: Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Widget> for Field {
|
|
||||||
fn from(value: Widget) -> Self {
|
|
||||||
Field {
|
|
||||||
widget: value.widget.to_string(),
|
|
||||||
label: value.label.map(|s| s.to_string()),
|
|
||||||
field_type: value.field_type.to_string(),
|
|
||||||
readonly: value.readonly,
|
|
||||||
required: value.required,
|
|
||||||
options: value
|
|
||||||
.options
|
|
||||||
.iter()
|
|
||||||
.map(|(key, val)| (key.to_string(), serde_json::json!(val)))
|
|
||||||
.collect::<serde_json::Map<String, Value>>()
|
|
||||||
.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct RepositoryItem {
|
|
||||||
pub fields: Value,
|
|
||||||
pub detail_url: Option<String>,
|
|
||||||
pub change_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum RepositoryResponse {
|
|
||||||
NoItem,
|
|
||||||
ItemOnly(RepositoryItem),
|
|
||||||
ItemAndHeaders(RepositoryItem, axum::http::HeaderMap),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<Option<RepositoryItem>> for RepositoryResponse {
|
|
||||||
fn into(self) -> Option<RepositoryItem> {
|
|
||||||
match self {
|
|
||||||
RepositoryResponse::NoItem => None,
|
|
||||||
RepositoryResponse::ItemOnly(some) => Some(some),
|
|
||||||
RepositoryResponse::ItemAndHeaders(some, _) => Some(some),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum RepositoryError {
|
|
||||||
// used internally.
|
|
||||||
#[error("key not found in downcast?")]
|
|
||||||
WrapperDowncastError,
|
|
||||||
|
|
||||||
// to be used by repositories:
|
|
||||||
#[error("repository item not found")]
|
|
||||||
ItemNotFound,
|
|
||||||
#[error("database error: {0}")]
|
|
||||||
DatabaseError(#[source] Box<dyn std::error::Error + Send + Sync>),
|
|
||||||
#[error("external error: {0}")]
|
|
||||||
ExternalError(#[source] Box<dyn std::error::Error + Send + Sync>),
|
|
||||||
|
|
||||||
// to be removed or refactored:
|
|
||||||
#[error("an unknown occurred: {0}")]
|
|
||||||
UnknownError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type RepositoryResult = Result<RepositoryResponse, RepositoryError>;
|
|
||||||
|
|
||||||
pub enum RepositoryList {
|
|
||||||
Empty,
|
|
||||||
List {
|
|
||||||
values: Vec<RepositoryItem>,
|
|
||||||
},
|
|
||||||
Page {
|
|
||||||
values: Vec<RepositoryItem>,
|
|
||||||
offset: usize,
|
|
||||||
total: usize,
|
|
||||||
},
|
|
||||||
Stream {
|
|
||||||
values: Vec<RepositoryItem>,
|
|
||||||
next_index: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoIterator for RepositoryList {
|
|
||||||
type Item = RepositoryItem;
|
|
||||||
type IntoIter = IntoIter<Self::Item>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
match self {
|
|
||||||
RepositoryList::Empty => vec![].into_iter(),
|
|
||||||
RepositoryList::List { values } => values.into_iter(),
|
|
||||||
RepositoryList::Page { values, .. } => values.into_iter(),
|
|
||||||
RepositoryList::Stream { values, .. } => values.into_iter(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for RepositoryList {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
RepositoryList::Empty => serializer.serialize_unit(),
|
|
||||||
RepositoryList::List { values }
|
|
||||||
| RepositoryList::Page { values, .. }
|
|
||||||
| RepositoryList::Stream { values, .. } => values.serialize(serializer),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Static initializer for RepositoryInfo.
|
|
||||||
pub struct RepoInfo {
|
|
||||||
pub name: &'static str,
|
|
||||||
pub lookup_key: &'static str,
|
|
||||||
pub display_list: &'static [&'static str],
|
|
||||||
pub fields: &'static [&'static str],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RepoInfo {
|
|
||||||
pub fn build(self) -> RepositoryInfo {
|
|
||||||
self.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct RepositoryInfo {
|
|
||||||
name: String,
|
|
||||||
lookup_key: String,
|
|
||||||
display_list: Vec<String>,
|
|
||||||
fields: Vec<(String, Field)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RepositoryInfo {
|
|
||||||
pub fn new(name: &str, lookup_key: &str) -> Self {
|
|
||||||
RepositoryInfo {
|
|
||||||
name: name.to_owned(),
|
|
||||||
lookup_key: lookup_key.to_owned(),
|
|
||||||
display_list: vec![],
|
|
||||||
fields: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// self mutating builder pattern
|
|
||||||
pub fn display_list(mut self, display_list: &[&str]) -> Self {
|
|
||||||
self.display_list = display_list.iter().map(|&e| e.to_string()).collect();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_widget<T: Into<Field>>(mut self, name: &str, item: T) -> Self {
|
|
||||||
let field = item.into(); // Convert the input into a Field
|
|
||||||
|
|
||||||
// Find the index of the existing entry with the same name, if it exists
|
|
||||||
let pos = self
|
|
||||||
.fields
|
|
||||||
.iter()
|
|
||||||
.position(|(existing_name, _)| existing_name == name);
|
|
||||||
|
|
||||||
match pos {
|
|
||||||
Some(index) => {
|
|
||||||
self.fields[index].1 = field;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
self.fields.push((name.to_owned(), field));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RepoInfo> for RepositoryInfo {
|
|
||||||
fn from(repo_info: RepoInfo) -> Self {
|
|
||||||
RepositoryInfo {
|
|
||||||
name: repo_info.name.to_string(),
|
|
||||||
lookup_key: repo_info.lookup_key.to_string(),
|
|
||||||
display_list: repo_info
|
|
||||||
.display_list
|
|
||||||
.iter()
|
|
||||||
.map(|&s| s.to_string())
|
|
||||||
.collect(),
|
|
||||||
fields: repo_info
|
|
||||||
.fields
|
|
||||||
.iter()
|
|
||||||
.map(|x| (x.to_string(), Field::from(Widget::default())))
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PrimaryKeyType: Any + Debug + Send + Sync {
|
|
||||||
fn as_any(&self) -> &dyn Any;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimaryKeyType for i64 {
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimaryKeyType for String {
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait DepotRepository: Send + Sync {
|
|
||||||
type Key: PrimaryKeyType;
|
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key>;
|
|
||||||
|
|
||||||
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
|
|
||||||
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
|
|
||||||
async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> RepositoryResult;
|
|
||||||
async fn create(&mut self, context: &RepositoryContext, data: Value) -> RepositoryResult;
|
|
||||||
async fn update(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &Self::Key,
|
|
||||||
data: Value,
|
|
||||||
) -> RepositoryResult;
|
|
||||||
async fn replace(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &Self::Key,
|
|
||||||
data: Value,
|
|
||||||
) -> RepositoryResult;
|
|
||||||
async fn delete(&mut self, context: &RepositoryContext, id: &Self::Key) -> Option<Value>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub(crate) trait DynDepotRepository: Send + Sync {
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>>;
|
|
||||||
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
|
|
||||||
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
|
|
||||||
async fn get(&self, context: &RepositoryContext, id: &dyn PrimaryKeyType) -> RepositoryResult;
|
|
||||||
async fn create(&mut self, context: &RepositoryContext, data: Value) -> RepositoryResult;
|
|
||||||
async fn update(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
data: Value,
|
|
||||||
) -> RepositoryResult;
|
|
||||||
async fn replace(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
data: Value,
|
|
||||||
) -> RepositoryResult;
|
|
||||||
async fn delete(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
) -> Option<Value>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DepotRepositoryWrapper<T: DepotRepository> {
|
|
||||||
inner: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: DepotRepository> DepotRepositoryWrapper<T> {
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<<T as DepotRepository>::Key> {
|
|
||||||
self.inner.key_from_string(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: DepotRepository> DynDepotRepository for DepotRepositoryWrapper<T> {
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>> {
|
|
||||||
if let Some(key) = self.inner.key_from_string(s) {
|
|
||||||
Some(Box::new(key))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo {
|
|
||||||
self.inner.info(context).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list(&self, context: &RepositoryContext) -> RepositoryList {
|
|
||||||
self.inner.list(context).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get(&self, context: &RepositoryContext, id: &dyn PrimaryKeyType) -> RepositoryResult {
|
|
||||||
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
|
||||||
self.inner.get(context, key).await
|
|
||||||
} else {
|
|
||||||
Err(RepositoryError::WrapperDowncastError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create(&mut self, context: &RepositoryContext, data: Value) -> RepositoryResult {
|
|
||||||
self.inner.create(context, data).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
data: Value,
|
|
||||||
) -> RepositoryResult {
|
|
||||||
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
|
||||||
self.inner.update(context, key, data).await
|
|
||||||
} else {
|
|
||||||
Err(RepositoryError::WrapperDowncastError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn replace(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
data: Value,
|
|
||||||
) -> RepositoryResult {
|
|
||||||
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
|
||||||
self.inner.replace(context, key, data).await
|
|
||||||
} else {
|
|
||||||
Err(RepositoryError::WrapperDowncastError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete(
|
|
||||||
&mut self,
|
|
||||||
context: &RepositoryContext,
|
|
||||||
id: &dyn PrimaryKeyType,
|
|
||||||
) -> Option<Value> {
|
|
||||||
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
|
|
||||||
self.inner.delete(context, key).await
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
use crate::service::templates::Templates;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use super::registry::DepotRegistry;
|
|
||||||
|
|
||||||
pub trait DepotState {
|
|
||||||
fn get_templates(&self) -> &Templates;
|
|
||||||
fn get_registry(&self) -> SharedDepotRegistry;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type SharedDepotRegistry = Arc<DepotRegistry>;
|
|
||||||
|
|
||||||
pub trait DepotFn = DepotState + Clone + Send + Sync + 'static;
|
|
@ -1,291 +0,0 @@
|
|||||||
use axum::extract::Path;
|
|
||||||
use axum::http::HeaderMap;
|
|
||||||
use axum::Form;
|
|
||||||
use axum::{extract::State, response::IntoResponse};
|
|
||||||
use log::info;
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use super::context::DepotContext;
|
|
||||||
use super::state::{DepotFn, DepotState};
|
|
||||||
|
|
||||||
pub fn base_template(headers: &HeaderMap) -> Option<String> {
|
|
||||||
let hx_request = headers.get("HX-Request").is_some();
|
|
||||||
if hx_request {
|
|
||||||
Some("depot/base_hx.jinja".to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn index<S: DepotFn>(depot: State<S>, headers: HeaderMap) -> impl IntoResponse {
|
|
||||||
let templates = depot.get_templates();
|
|
||||||
let registry = depot.get_registry();
|
|
||||||
templates.render_html(
|
|
||||||
"depot/index.html",
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index Action is POST to the index site. We can anchor some general business code here.
|
|
||||||
pub async fn index_action<S: DepotFn>(depot: State<S>) -> impl IntoResponse {
|
|
||||||
"There is your answer!".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_section<S: DepotFn>(
|
|
||||||
depot: State<S>,
|
|
||||||
Path(depot_key): Path<String>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = depot.get_templates();
|
|
||||||
templates.render_html("depot/depot.jinja", ())
|
|
||||||
}
|
|
||||||
|
|
||||||
// List Items renders the entire list item page.
|
|
||||||
pub async fn list_item_collection<S: DepotFn>(
|
|
||||||
depot: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path((depot_key, model_key)): Path<(String, String)>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
info!("list_item_collection {} for model {}", depot_key, model_key);
|
|
||||||
let templates = depot.get_templates();
|
|
||||||
let registry = depot.get_registry();
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
|
||||||
let repo = repo.lock().await;
|
|
||||||
let depot_model = registry
|
|
||||||
.get_model(&depot_key, &model_key)
|
|
||||||
.expect("depot Model not found?"); // we will need a proper error route; so something that implements IntoResponse and can be substituted in the unwraps and expects.
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
item_info: Some(repo.info(&depot_model).await),
|
|
||||||
item_list: repo.list(&depot_model).await,
|
|
||||||
item_model: Some(depot_model),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
templates.render_html("depot/items/item_list.jinja", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Items Action is a POST to an item list. By default these are actions, that work on a list of items as input.
|
|
||||||
pub async fn item_collection_action<S: DepotFn>(
|
|
||||||
depot: State<S>,
|
|
||||||
Path((depot_key, model_key)): Path<(String, String)>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
"There is your answer!".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Item Details shows one single dataset.
|
|
||||||
pub async fn view_item_details<S: DepotFn>(
|
|
||||||
depot: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path((depot_key, model_key, id)): Path<(String, String, String)>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = depot.get_templates();
|
|
||||||
let registry = depot.get_registry();
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
|
||||||
let repo = repo.lock().await;
|
|
||||||
let depot_model = registry
|
|
||||||
.get_model(&depot_key, &model_key)
|
|
||||||
.expect("depot Model not found?");
|
|
||||||
if let Some(key) = repo.key_from_string(id) {
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
item_info: Some(repo.info(&depot_model).await),
|
|
||||||
item_list: repo.list(&depot_model).await,
|
|
||||||
item: repo.get(&depot_model, key.as_ref()).await.unwrap().into(),
|
|
||||||
item_model: Some(depot_model),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
templates.render_html("depot/items/item_detail.jinja", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn new_item<S: DepotFn>(
|
|
||||||
depot: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path((depot_key, model_key)): Path<(String, String)>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = depot.get_templates();
|
|
||||||
let registry = depot.get_registry();
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
|
||||||
let repo = repo.lock().await;
|
|
||||||
let depot_model = registry
|
|
||||||
.get_model(&depot_key, &model_key)
|
|
||||||
.expect("depot Model not found?");
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
item_info: Some(repo.info(&depot_model).await),
|
|
||||||
item_list: repo.list(&depot_model).await,
|
|
||||||
item_model: Some(depot_model),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
templates.render_html("depot/items/item_create.jinja", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_item<S: DepotFn>(
|
|
||||||
depot: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path((depot_key, model_key)): Path<(String, String)>,
|
|
||||||
Form(form): Form<Value>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = depot.get_templates();
|
|
||||||
let registry = depot.get_registry();
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
|
||||||
let mut repo = repo.lock().await;
|
|
||||||
let depot_model = registry
|
|
||||||
.get_model(&depot_key, &model_key)
|
|
||||||
.expect("Depot Model not found?");
|
|
||||||
|
|
||||||
// create our item.
|
|
||||||
let result = repo.create(&depot_model, form).await;
|
|
||||||
|
|
||||||
// TODO: refactor run over these views, way too much repetition.
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
item_info: Some(repo.info(&depot_model).await),
|
|
||||||
item_list: repo.list(&depot_model).await,
|
|
||||||
item_model: Some(depot_model),
|
|
||||||
item: result.unwrap().into(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
templates.render_html("depot/items/item_create.jinja", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change is the GET version.
|
|
||||||
pub async fn change_item<S: DepotFn>(
|
|
||||||
depot: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path((depot_key, model_key, id)): Path<(String, String, String)>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = depot.get_templates();
|
|
||||||
let registry = depot.get_registry();
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
|
||||||
let repo = repo.lock().await;
|
|
||||||
let depot_model = registry
|
|
||||||
.get_model(&depot_key, &model_key)
|
|
||||||
.expect("Depot Model not found?");
|
|
||||||
if let Some(key) = repo.key_from_string(id) {
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
item_info: Some(repo.info(&depot_model).await),
|
|
||||||
item_list: repo.list(&depot_model).await,
|
|
||||||
item: repo.get(&depot_model, key.as_ref()).await.unwrap().into(),
|
|
||||||
item_model: Some(depot_model),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Repository did not have key.
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Repository could not be loaded.
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
templates.render_html("depot/items/item_change.jinja", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_item<S: DepotFn>(
|
|
||||||
depot: State<S>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
Path((depot_key, model_key, id)): Path<(String, String, String)>,
|
|
||||||
Form(form): Form<Value>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = depot.get_templates();
|
|
||||||
let registry = depot.get_registry();
|
|
||||||
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
|
|
||||||
let mut repo = repo.lock().await;
|
|
||||||
let depot_model = registry
|
|
||||||
.get_model(&depot_key, &model_key)
|
|
||||||
.expect("depot Model not found?");
|
|
||||||
if let Some(key) = repo.key_from_string(id) {
|
|
||||||
let result = repo.update(&depot_model, key.as_ref(), form).await;
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
item_info: Some(repo.info(&depot_model).await),
|
|
||||||
item_list: repo.list(&depot_model).await,
|
|
||||||
item: result.unwrap().into(),
|
|
||||||
item_model: Some(depot_model),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DepotContext {
|
|
||||||
base: base_template(&headers),
|
|
||||||
sections: registry.get_sections(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let response = templates.render_html("depot/items/item_change.jinja", context);
|
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
// Item Action allows running an action on one single dataset.
|
|
||||||
pub async fn item_action<S: DepotFn>(
|
|
||||||
depot: State<S>,
|
|
||||||
Path((depot_key, model_key, model_id)): Path<(String, String, String)>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
"There is your answer!".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn debug_view<S: DepotFn>(
|
|
||||||
depot: State<S>,
|
|
||||||
Path(data): Path<String>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
println!("debug: {}", data);
|
|
||||||
"Debug!".to_owned()
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
/// This is a static configuration object.
|
|
||||||
/// It might be changed in the future to have a dynamic counterpart.
|
|
||||||
///
|
|
||||||
/// ## Example:
|
|
||||||
/// Creating a simple required and readonly text input field with a label:
|
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
/// let my_field = Field::widget("/admin/widgets/input_text.jinja")
|
|
||||||
/// .labelled("Username")
|
|
||||||
/// .required()
|
|
||||||
/// .readonly();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Serialize, Clone, Copy)]
|
|
||||||
pub struct Widget {
|
|
||||||
pub widget: &'static str,
|
|
||||||
pub label: Option<&'static str>,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub field_type: &'static str,
|
|
||||||
pub required: bool,
|
|
||||||
pub readonly: bool,
|
|
||||||
pub options: &'static [(&'static str, &'static str)],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget {
|
|
||||||
pub fn widget(widget: &'static str) -> Self {
|
|
||||||
Widget {
|
|
||||||
widget: widget,
|
|
||||||
label: None,
|
|
||||||
field_type: "text",
|
|
||||||
required: false,
|
|
||||||
readonly: false,
|
|
||||||
options: &[],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default() -> Self {
|
|
||||||
Self::widget("/depot/widgets/input_text.jinja")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn textarea() -> Self {
|
|
||||||
Self::widget("/depot/widgets/input_textarea.jinja")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn checkbox() -> Self {
|
|
||||||
Self::widget("/depot/widgets/checkbox_toggle.jinja")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn required(mut self) -> Self {
|
|
||||||
self.required = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readonly(mut self) -> Self {
|
|
||||||
self.readonly = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn labeled(mut self, label: &'static str) -> Self {
|
|
||||||
self.label = Some(label);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_password(mut self) -> Self {
|
|
||||||
self.field_type = "password";
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_hidden(mut self) -> Self {
|
|
||||||
self.field_type = "hidden";
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn options(mut self, options: &'static [(&'static str, &'static str)]) -> Self {
|
|
||||||
self.options = options;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +1,3 @@
|
|||||||
#![feature(trait_alias)]
|
pub mod admin;
|
||||||
pub mod depot;
|
pub mod auth;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
@ -3,23 +3,7 @@ use axum::response::Html;
|
|||||||
use minijinja::{path_loader, Environment, Value};
|
use minijinja::{path_loader, Environment, Value};
|
||||||
use minijinja_autoreload::AutoReloader;
|
use minijinja_autoreload::AutoReloader;
|
||||||
use pulldown_cmark::Event;
|
use pulldown_cmark::Event;
|
||||||
use std::{path::Path, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub fn composite_loader<P: AsRef<Path>>(
|
|
||||||
dirs: Vec<P>,
|
|
||||||
) -> impl Fn(&str) -> Result<Option<String>, minijinja::Error> + Send + Sync + 'static {
|
|
||||||
let loaders: Vec<_> = dirs.into_iter().map(|dir| path_loader(dir)).collect();
|
|
||||||
move |name| {
|
|
||||||
for loader in &loaders {
|
|
||||||
match loader(name) {
|
|
||||||
Ok(Some(template)) => return Ok(Some(template)),
|
|
||||||
Ok(None) => continue,
|
|
||||||
Err(err) => return Err(err.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Templates {
|
pub struct Templates {
|
||||||
@ -31,8 +15,7 @@ impl Templates {
|
|||||||
let reloader = AutoReloader::new(move |notifier| {
|
let reloader = AutoReloader::new(move |notifier| {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
let template_path = "templates";
|
let template_path = "templates";
|
||||||
let loader = composite_loader(vec![template_path]);
|
environment.set_loader(path_loader(&template_path));
|
||||||
environment.set_loader(loader);
|
|
||||||
environment.add_filter("none", none);
|
environment.add_filter("none", none);
|
||||||
environment.add_filter("markdown", markdown);
|
environment.add_filter("markdown", markdown);
|
||||||
environment.add_filter("yesno", filter_yesno);
|
environment.add_filter("yesno", filter_yesno);
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rear_auth"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
default-run = "rear_auth"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "rear_auth"
|
|
||||||
path = "src/lib.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
entity = { path = "../entity" }
|
|
||||||
rear = { path = "../rear" }
|
|
||||||
anyhow = "1.0.75"
|
|
||||||
axum = "0.7"
|
|
||||||
serde = { version = "1.0.188", features = ["derive"] }
|
|
||||||
tokio = { version = "1.32.0", features = ["full"] }
|
|
||||||
log = "0.4.20"
|
|
||||||
serde_json = "1.0.108"
|
|
||||||
sea-orm = { version = "0.12.10", features = [
|
|
||||||
"runtime-tokio-native-tls",
|
|
||||||
"sqlx-postgres",
|
|
||||||
] }
|
|
||||||
axum-login = "0.15.3"
|
|
||||||
axum-messages = "0.6.1"
|
|
||||||
async-trait = "0.1.80"
|
|
||||||
password-auth = "1.0.0"
|
|
||||||
thiserror = "1.0.61"
|
|
||||||
blake3 = "1.5.1"
|
|
||||||
chrono = "0.4"
|
|
@ -1,2 +0,0 @@
|
|||||||
Atm. it is not clear to me if rear_auth will really be it's own package, as it needs to interact a lot with rear, or if I split up rear into modules instead.
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
|||||||
pub mod models;
|
|
||||||
pub mod user_admin_repository;
|
|
||||||
pub mod utils;
|
|
||||||
pub mod views;
|
|
@ -1 +0,0 @@
|
|||||||
pub fn main() {}
|
|
@ -1,248 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use axum_login::{AuthUser, AuthzBackend};
|
|
||||||
use axum_login::{AuthnBackend, UserId};
|
|
||||||
use log::debug;
|
|
||||||
use password_auth::{generate_hash, is_hash_obsolete, verify_password};
|
|
||||||
use sea_orm::{
|
|
||||||
ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait,
|
|
||||||
QueryFilter,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use thiserror::Error;
|
|
||||||
use tokio::task;
|
|
||||||
|
|
||||||
use crate::utils::get_current_timestamp;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct UserRepository {
|
|
||||||
pub(crate) connection: DatabaseConnection,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserRepository {
|
|
||||||
pub fn new(connection: DatabaseConnection) -> Self {
|
|
||||||
UserRepository {
|
|
||||||
connection: connection,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn encode_password(password: String) -> String {
|
|
||||||
// This function will try to avoid re-encoding an encoded password.
|
|
||||||
// This is why it is not public outside of the crate.
|
|
||||||
if let Ok(_is_obsolete) = is_hash_obsolete(password.as_str()) {
|
|
||||||
// Not sure what to do if it is obsolete.
|
|
||||||
if _is_obsolete {
|
|
||||||
debug!("UserRepository::encode_password: found obsolete password hash.");
|
|
||||||
}
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
// As checking for obsoleteness errored out, we assume this a raw password.
|
|
||||||
generate_hash(password)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_user(username: &str, password: &str) -> entity::user::ActiveModel {
|
|
||||||
entity::user::ActiveModel {
|
|
||||||
username: sea_orm::ActiveValue::Set(username.to_owned()),
|
|
||||||
password: sea_orm::ActiveValue::Set(UserRepository::encode_password(
|
|
||||||
password.to_owned(),
|
|
||||||
)),
|
|
||||||
date_joined: sea_orm::ActiveValue::Set(Some(get_current_timestamp())),
|
|
||||||
..<entity::user::ActiveModel as ActiveModelTrait>::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
|
||||||
pub struct AuthenticatedUser {
|
|
||||||
id: i64,
|
|
||||||
pub username: String,
|
|
||||||
password: String,
|
|
||||||
session_token: Vec<u8>, // Stores the hash
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthenticatedUser {
|
|
||||||
fn new(id: i64, username: String, password: String) -> Self {
|
|
||||||
AuthenticatedUser {
|
|
||||||
id: id,
|
|
||||||
username: username,
|
|
||||||
session_token: blake3::hash(&password.as_bytes()).as_bytes().to_vec(),
|
|
||||||
password: password,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_password(&mut self, new_password: String) {
|
|
||||||
self.password = new_password;
|
|
||||||
self.session_token = blake3::hash(self.password.as_bytes()).as_bytes().to_vec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here we've implemented `Debug` manually to avoid accidentally logging the
|
|
||||||
// password hash.
|
|
||||||
impl std::fmt::Debug for AuthenticatedUser {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("User")
|
|
||||||
.field("id", &self.id)
|
|
||||||
.field("username", &self.username)
|
|
||||||
.field("password", &"[redacted]")
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthUser for AuthenticatedUser {
|
|
||||||
type Id = i64;
|
|
||||||
|
|
||||||
fn id(&self) -> Self::Id {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn session_auth_hash(&self) -> &[u8] {
|
|
||||||
&self.session_token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<entity::user::Model> for AuthenticatedUser {
|
|
||||||
fn from(value: entity::user::Model) -> Self {
|
|
||||||
AuthenticatedUser::new(value.id, value.username, value.password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This allows us to extract the authentication fields from forms. We use this
|
|
||||||
// to authenticate requests with the backend.
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Credentials {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
pub next: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub struct NotFoundError {
|
|
||||||
pub details: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NotFoundError {
|
|
||||||
pub fn new(details: &str) -> Self {
|
|
||||||
NotFoundError {
|
|
||||||
details: details.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for NotFoundError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Not Found... Error: {}", self.details)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error(transparent)]
|
|
||||||
DbErr(#[from] sea_orm::DbErr),
|
|
||||||
|
|
||||||
#[error("Not Found: {0}")]
|
|
||||||
NotFound(#[from] NotFoundError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
TaskJoin(#[from] task::JoinError),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AuthnBackend for UserRepository {
|
|
||||||
type User = AuthenticatedUser;
|
|
||||||
type Credentials = Credentials;
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
async fn authenticate(
|
|
||||||
&self,
|
|
||||||
creds: Self::Credentials,
|
|
||||||
) -> Result<Option<Self::User>, Self::Error> {
|
|
||||||
let user_found = entity::User::find()
|
|
||||||
.filter(entity::user::Column::Username.eq(creds.username))
|
|
||||||
.one(&self.connection)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let Some(user) = user_found {
|
|
||||||
let given_password = creds.password.clone();
|
|
||||||
let user_password = user.password.clone();
|
|
||||||
let verified =
|
|
||||||
task::spawn_blocking(move || verify_password(&given_password, &user_password))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if verified.is_ok() {
|
|
||||||
let mut db_user: entity::user::ActiveModel = user.into();
|
|
||||||
db_user.last_login = ActiveValue::Set(Some(get_current_timestamp()));
|
|
||||||
let user = db_user.update(&self.connection).await?;
|
|
||||||
let rear_user: AuthenticatedUser = user.into();
|
|
||||||
return Ok(Some(rear_user));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None) // No user found or verification failed
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
|
|
||||||
let user_found = entity::User::find_by_id(*user_id)
|
|
||||||
.one(&self.connection)
|
|
||||||
.await?;
|
|
||||||
if let Some(user) = user_found {
|
|
||||||
let rear_user: AuthenticatedUser = user.into();
|
|
||||||
Ok(Some(rear_user))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
|
||||||
pub struct Permission {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for Permission {
|
|
||||||
fn from(name: &str) -> Self {
|
|
||||||
Permission {
|
|
||||||
name: name.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<entity::permission::Model> for Permission {
|
|
||||||
fn from(model: entity::permission::Model) -> Self {
|
|
||||||
Permission {
|
|
||||||
name: model.codename,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AuthzBackend for UserRepository {
|
|
||||||
type Permission = Permission;
|
|
||||||
|
|
||||||
async fn get_group_permissions(
|
|
||||||
&self,
|
|
||||||
user: &Self::User,
|
|
||||||
) -> Result<HashSet<Self::Permission>, Self::Error> {
|
|
||||||
let user = entity::User::find_by_id(user.id)
|
|
||||||
.one(&self.connection)
|
|
||||||
.await?;
|
|
||||||
if let Some(user) = user {
|
|
||||||
let permissions = user
|
|
||||||
.find_related(entity::Permission)
|
|
||||||
.all(&self.connection)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(permissions
|
|
||||||
.into_iter()
|
|
||||||
.map(|item| Permission::from(item))
|
|
||||||
.collect())
|
|
||||||
} else {
|
|
||||||
Ok(HashSet::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use a type alias for convenience.
|
|
||||||
//
|
|
||||||
// Note that we've supplied our concrete backend here.
|
|
||||||
pub type AuthSession = axum_login::AuthSession<UserRepository>;
|
|
@ -1,224 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use log::{debug, warn};
|
|
||||||
use rear::depot::prelude::*;
|
|
||||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set};
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use crate::models::UserRepository;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl DepotRepository for UserRepository {
|
|
||||||
type Key = i64;
|
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
|
||||||
if let Ok(i) = s.parse::<i64>() {
|
|
||||||
Some(i)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn info(&self, _: &RepositoryContext) -> RepositoryInfo {
|
|
||||||
RepoInfo {
|
|
||||||
name: "User",
|
|
||||||
lookup_key: "id",
|
|
||||||
display_list: &["id", "username"],
|
|
||||||
fields: &[
|
|
||||||
"username",
|
|
||||||
"password",
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"email",
|
|
||||||
"is_staff",
|
|
||||||
"is_active",
|
|
||||||
"is_superuser",
|
|
||||||
],
|
|
||||||
// fields_readonly: &["last_login", "date_joined"]
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
.set_widget(
|
|
||||||
"password",
|
|
||||||
Widget::widget("/depot/widgets/password_change.jinja").as_password(),
|
|
||||||
)
|
|
||||||
.set_widget("is_staff", Widget::checkbox())
|
|
||||||
.set_widget("is_active", Widget::checkbox())
|
|
||||||
.set_widget("is_superuser", Widget::checkbox())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
|
|
||||||
let id: i32 = *id as i32; // use try_into() instead.
|
|
||||||
let get_user = entity::User::find_by_id(id).one(&self.connection).await;
|
|
||||||
if let Ok(get_user) = get_user {
|
|
||||||
if let Some(user) = get_user {
|
|
||||||
let id = user.id.to_string();
|
|
||||||
match serde_json::to_value(&user) {
|
|
||||||
Ok(item) => {
|
|
||||||
return Ok(RepositoryResponse::ItemOnly(model.build_item(&*id, item)));
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
return Err(RepositoryError::UnknownError(
|
|
||||||
"JSON Error creating value".to_owned(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(RepositoryResponse::NoItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
|
||||||
let results = if let Ok(results) = entity::User::find().all(&self.connection).await {
|
|
||||||
results
|
|
||||||
} else {
|
|
||||||
return RepositoryList::Empty;
|
|
||||||
};
|
|
||||||
let repository_items: Vec<RepositoryItem> = results
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|item| match serde_json::to_value(&item) {
|
|
||||||
Ok(fields) => {
|
|
||||||
let id = item.id.to_string();
|
|
||||||
Some(model.build_item(&*id, fields))
|
|
||||||
}
|
|
||||||
Err(_) => None,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
RepositoryList::List {
|
|
||||||
values: repository_items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create(&mut self, model: &RepositoryContext, data: Value) -> RepositoryResult {
|
|
||||||
if let Value::Object(data) = data {
|
|
||||||
let username = data.get("username").unwrap().as_str().unwrap();
|
|
||||||
let password = data.get("password").unwrap().as_str().unwrap();
|
|
||||||
|
|
||||||
let mut user = UserRepository::new_user(username, password);
|
|
||||||
|
|
||||||
let keys = [
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"email",
|
|
||||||
"is_staff",
|
|
||||||
"is_active",
|
|
||||||
"is_superuser",
|
|
||||||
];
|
|
||||||
|
|
||||||
for key in &keys {
|
|
||||||
if let Some(value) = data.get(*key) {
|
|
||||||
match *key {
|
|
||||||
"first_name" => user.first_name = Set(value.as_str().map(|s| s.to_owned())),
|
|
||||||
"last_name" => user.last_name = Set(value.as_str().map(|s| s.to_owned())),
|
|
||||||
"email" => user.email = Set(value.as_str().map(|s| s.to_owned())),
|
|
||||||
"is_staff" => user.is_staff = Set(value.as_bool().unwrap_or(false)),
|
|
||||||
"is_active" => user.is_active = Set(value.as_bool().unwrap_or(true)),
|
|
||||||
"is_superuser" => user.is_superuser = Set(value.as_bool().unwrap_or(false)),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(user) = user.insert(&self.connection).await {
|
|
||||||
let id = user.id.to_string();
|
|
||||||
let item = model.build_item(&*id, serde_json::to_value(&user).unwrap());
|
|
||||||
return Ok(RepositoryResponse::ItemOnly(item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(RepositoryResponse::NoItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update(
|
|
||||||
&mut self,
|
|
||||||
model: &RepositoryContext,
|
|
||||||
id: &Self::Key,
|
|
||||||
data: Value,
|
|
||||||
) -> RepositoryResult {
|
|
||||||
let id: i32 = *id as i32;
|
|
||||||
let user: Option<entity::user::Model> = entity::User::find_by_id(id)
|
|
||||||
.one(&self.connection)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RepositoryError::DatabaseError(Box::new(e)))?;
|
|
||||||
let mut user: entity::user::ActiveModel = user.ok_or(RepositoryError::ItemNotFound)?.into();
|
|
||||||
|
|
||||||
// should we really allow username change?
|
|
||||||
if let Some(value) = data.get("username") {
|
|
||||||
if let Some(value) = value.as_str() {
|
|
||||||
user.username = Set(value.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let keys = [
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"email",
|
|
||||||
"is_staff",
|
|
||||||
"is_active",
|
|
||||||
"is_superuser",
|
|
||||||
];
|
|
||||||
|
|
||||||
for key in &keys {
|
|
||||||
if let Some(value) = data.get(*key) {
|
|
||||||
match *key {
|
|
||||||
"first_name" => user.first_name = Set(value.as_str().map(|s| s.to_owned())),
|
|
||||||
"last_name" => user.last_name = Set(value.as_str().map(|s| s.to_owned())),
|
|
||||||
"email" => user.email = Set(value.as_str().map(|s| s.to_owned())),
|
|
||||||
"is_staff" => user.is_staff = Set(value.as_bool().unwrap_or(false)),
|
|
||||||
"is_active" => user.is_active = Set(value.as_bool().unwrap_or(true)),
|
|
||||||
"is_superuser" => user.is_superuser = Set(value.as_bool().unwrap_or(false)),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update
|
|
||||||
match user.update(&self.connection).await {
|
|
||||||
Ok(user) => {
|
|
||||||
let id = user.id.to_string();
|
|
||||||
return Ok(RepositoryResponse::ItemOnly(
|
|
||||||
model.build_item(&*id, serde_json::to_value(&user).unwrap()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Error updating user");
|
|
||||||
return Err(RepositoryError::DatabaseError(Box::new(err)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn replace(
|
|
||||||
&mut self,
|
|
||||||
model: &RepositoryContext,
|
|
||||||
id: &Self::Key,
|
|
||||||
data: Value,
|
|
||||||
) -> RepositoryResult {
|
|
||||||
self.update(model, id, data).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete(&mut self, _: &RepositoryContext, id: &Self::Key) -> Option<Value> {
|
|
||||||
let id: i32 = *id as i32;
|
|
||||||
let user: Option<entity::user::Model> = entity::User::find_by_id(id)
|
|
||||||
.one(&self.connection)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
if let Some(user) = user {
|
|
||||||
let delete_result = user.delete(&self.connection).await.unwrap();
|
|
||||||
// .ok_or(RepositoryError::DatabaseError(Box::new(err)))?;
|
|
||||||
debug!("deleted rows: {}", delete_result.rows_affected);
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register(registry: &mut DepotRegistry, db: DatabaseConnection) -> UserRepository {
|
|
||||||
let section_key = registry.register_section("Auth");
|
|
||||||
let repo = UserRepository::new(db);
|
|
||||||
let model_config = DepotModelConfig {
|
|
||||||
section_key: section_key,
|
|
||||||
name: "User".to_owned(),
|
|
||||||
};
|
|
||||||
let model_result = registry.register_model(model_config, repo.clone());
|
|
||||||
match model_result {
|
|
||||||
Err(err) => panic!("{}", err),
|
|
||||||
_ => repo,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
use sea_orm::prelude::DateTimeWithTimeZone;
|
|
||||||
|
|
||||||
pub fn get_current_timestamp() -> DateTimeWithTimeZone {
|
|
||||||
let utc_now = chrono::Utc::now();
|
|
||||||
// Apply the timezone offset to the UTC time: FixedOffset::east(offset_hours * 3600);
|
|
||||||
let local_time = utc_now;
|
|
||||||
local_time.into()
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
use axum::{
|
|
||||||
extract::Query,
|
|
||||||
http::StatusCode,
|
|
||||||
response::{IntoResponse, Redirect},
|
|
||||||
routing::{get, post},
|
|
||||||
Form, Router,
|
|
||||||
};
|
|
||||||
use axum_messages::{Message, Messages};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::models::{AuthSession, Credentials};
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct LoginTemplate {
|
|
||||||
messages: Vec<Message>,
|
|
||||||
next: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// This allows us to extract the "next" field from the query string. We use this
|
|
||||||
// to redirect after log in.
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct NextUrl {
|
|
||||||
next: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn routes<S: rear::depot::state::DepotState + Clone + Send + Sync + 'static>() -> Router<S>
|
|
||||||
where {
|
|
||||||
Router::new()
|
|
||||||
.route("/login", post(self::post::login))
|
|
||||||
.route("/login", get(self::get::login::<S>))
|
|
||||||
.route("/logout", post(self::post::logout))
|
|
||||||
}
|
|
||||||
|
|
||||||
mod post {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub async fn login(
|
|
||||||
mut auth_session: AuthSession,
|
|
||||||
messages: Messages,
|
|
||||||
Form(creds): Form<Credentials>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let user = match auth_session.authenticate(creds.clone()).await {
|
|
||||||
Ok(Some(user)) => user,
|
|
||||||
Ok(None) => {
|
|
||||||
messages.error("Invalid credentials");
|
|
||||||
|
|
||||||
let mut login_url = "/depot/login".to_string();
|
|
||||||
if let Some(next) = creds.next {
|
|
||||||
login_url = format!("{}?next={}", login_url, next);
|
|
||||||
};
|
|
||||||
|
|
||||||
return Redirect::to(&login_url).into_response();
|
|
||||||
}
|
|
||||||
Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if auth_session.login(&user).await.is_err() {
|
|
||||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.success(format!("Successfully logged in as {}", user.username));
|
|
||||||
|
|
||||||
if let Some(ref next) = creds.next {
|
|
||||||
Redirect::to(next)
|
|
||||||
} else {
|
|
||||||
Redirect::to("/")
|
|
||||||
}
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
|
|
||||||
match auth_session.logout().await {
|
|
||||||
Ok(_) => Redirect::to("/depot/login").into_response(),
|
|
||||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod get {
|
|
||||||
use super::*;
|
|
||||||
use axum::extract::State;
|
|
||||||
|
|
||||||
pub async fn login<S: rear::depot::state::DepotState + Clone + Send + Sync + 'static>(
|
|
||||||
messages: Messages,
|
|
||||||
admin: State<S>,
|
|
||||||
Query(NextUrl { next }): Query<NextUrl>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let templates = admin.get_templates();
|
|
||||||
let context = LoginTemplate {
|
|
||||||
messages: messages.into_iter().collect(),
|
|
||||||
next,
|
|
||||||
};
|
|
||||||
templates.render_html("depot/login.html", context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this was taken from the axum_login examples and modified. Redirection might need reverse-routing support.
|
|
@ -1,5 +1,5 @@
|
|||||||
|
use crate::admin::domain::*;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rear::depot::prelude::*;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
struct Repository {}
|
struct Repository {}
|
||||||
@ -7,7 +7,7 @@ struct Repository {}
|
|||||||
impl Repository {}
|
impl Repository {}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl DepotRepository for Repository {
|
impl AdminRepository for Repository {
|
||||||
type Key = i64;
|
type Key = i64;
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||||
@ -34,13 +34,17 @@ impl DepotRepository for Repository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// POST on item collection.
|
// POST on item collection.
|
||||||
async fn create(&mut self, model: &RepositoryContext, mut data: Value) -> RepositoryResult {
|
async fn create(
|
||||||
Ok(RepositoryResponse::NoItem)
|
&mut self,
|
||||||
|
model: &RepositoryContext,
|
||||||
|
mut data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET single item.
|
// GET single item.
|
||||||
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
|
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> {
|
||||||
Ok(RepositoryResponse::NoItem)
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH single item.
|
// PATCH single item.
|
||||||
@ -49,8 +53,8 @@ impl DepotRepository for Repository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> RepositoryResult {
|
) -> Option<RepositoryItem> {
|
||||||
Ok(RepositoryResponse::NoItem)
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT single item.
|
// PUT single item.
|
||||||
@ -59,8 +63,8 @@ impl DepotRepository for Repository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> RepositoryResult {
|
) -> Option<RepositoryItem> {
|
||||||
Ok(RepositoryResponse::NoItem)
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE single item.
|
// DELETE single item.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
use crate::admin::domain::*;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rear::depot::prelude::*;
|
use rear::admin::state::AdminRegistry;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ impl FileRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl DepotRepository for FileRepository {
|
impl AdminRepository for FileRepository {
|
||||||
type Key = String;
|
type Key = String;
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||||
@ -61,13 +62,17 @@ impl DepotRepository for FileRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// POST on item collection.
|
// POST on item collection.
|
||||||
async fn create(&mut self, model: &RepositoryContext, mut data: Value) -> RepositoryResult {
|
async fn create(
|
||||||
Ok(RepositoryResponse::NoItem)
|
&mut self,
|
||||||
|
model: &RepositoryContext,
|
||||||
|
mut data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET single item.
|
// GET single item.
|
||||||
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
|
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> {
|
||||||
Ok(RepositoryResponse::NoItem)
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH single item.
|
// PATCH single item.
|
||||||
@ -76,8 +81,8 @@ impl DepotRepository for FileRepository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> RepositoryResult {
|
) -> Option<RepositoryItem> {
|
||||||
Ok(RepositoryResponse::NoItem)
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT single item.
|
// PUT single item.
|
||||||
@ -86,8 +91,8 @@ impl DepotRepository for FileRepository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> RepositoryResult {
|
) -> Option<RepositoryItem> {
|
||||||
Ok(RepositoryResponse::NoItem)
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE single item.
|
// DELETE single item.
|
||||||
@ -96,11 +101,11 @@ impl DepotRepository for FileRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(registry: &mut DepotRegistry, path: &str) {
|
pub fn register(registry: &mut AdminRegistry, path: &str) {
|
||||||
let section_key = registry.register_section("Files");
|
let app_key = registry.register_app("Files");
|
||||||
let repo = FileRepository::new(path);
|
let repo = FileRepository::new(path);
|
||||||
let model_config = DepotModelConfig {
|
let model_config = AdminModelConfig {
|
||||||
section_key: section_key,
|
app_key: app_key,
|
||||||
name: "Files".to_owned(),
|
name: "Files".to_owned(),
|
||||||
};
|
};
|
||||||
let model_result = registry.register_model(model_config, repo);
|
let model_result = registry.register_model(model_config, repo);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
pub mod empty_repository;
|
pub mod empty_repository;
|
||||||
pub mod file_repository;
|
|
||||||
pub mod static_repository;
|
pub mod static_repository;
|
||||||
|
pub mod user_repository;
|
||||||
|
pub mod file_repository;
|
@ -1,6 +1,7 @@
|
|||||||
use crate::depot::prelude::*;
|
use crate::admin::domain::*;
|
||||||
|
use crate::admin::state::AdminRegistry;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::debug;
|
use log::{debug, warn};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
/// This is a showcase implementation with a static repository
|
/// This is a showcase implementation with a static repository
|
||||||
@ -34,7 +35,7 @@ impl MyStaticRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl DepotRepository for MyStaticRepository {
|
impl AdminRepository for MyStaticRepository {
|
||||||
type Key = i64;
|
type Key = i64;
|
||||||
|
|
||||||
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||||
@ -57,13 +58,11 @@ impl DepotRepository for MyStaticRepository {
|
|||||||
//.set_widget("name", Widget::textarea().options(&[("disabled", "true")]))
|
//.set_widget("name", Widget::textarea().options(&[("disabled", "true")]))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
|
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> {
|
||||||
let id = *id as usize;
|
let id = *id as usize;
|
||||||
let item = self.content.get(id - 1).cloned().unwrap();
|
let item = self.content.get(id - 1).cloned().unwrap();
|
||||||
let id = item.get("id").unwrap();
|
let id = item.get("id").unwrap();
|
||||||
Ok(RepositoryResponse::ItemOnly(
|
Some(model.build_item(&*id.to_string(), item))
|
||||||
model.build_item(&*id.to_string(), item),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
||||||
@ -72,21 +71,16 @@ impl DepotRepository for MyStaticRepository {
|
|||||||
.content
|
.content
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|item| match item.get("id") {
|
.map(|item| model.build_item(&*item.get("id").unwrap().to_string(), item))
|
||||||
Some(id_value) => {
|
|
||||||
let id_str = id_value.to_string();
|
|
||||||
Some(model.build_item(&id_str, item))
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
eprintln!("Skipping item due to missing 'id'");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create(&mut self, model: &RepositoryContext, mut data: Value) -> RepositoryResult {
|
async fn create(
|
||||||
|
&mut self,
|
||||||
|
model: &RepositoryContext,
|
||||||
|
mut data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
debug!("Asked to create: {}", data);
|
debug!("Asked to create: {}", data);
|
||||||
|
|
||||||
let new_id = self.next_id;
|
let new_id = self.next_id;
|
||||||
@ -100,9 +94,7 @@ impl DepotRepository for MyStaticRepository {
|
|||||||
self.content.push(data.clone());
|
self.content.push(data.clone());
|
||||||
|
|
||||||
// Return the newly created item
|
// Return the newly created item
|
||||||
Ok(RepositoryResponse::ItemOnly(
|
Some(model.build_item(&*new_id.to_string(), data))
|
||||||
model.build_item(&*new_id.to_string(), data),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update(
|
async fn update(
|
||||||
@ -110,7 +102,7 @@ impl DepotRepository for MyStaticRepository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> RepositoryResult {
|
) -> Option<RepositoryItem> {
|
||||||
debug!("I would now update: {}, {}", id, data);
|
debug!("I would now update: {}, {}", id, data);
|
||||||
|
|
||||||
// First, find the index of the item to update
|
// First, find the index of the item to update
|
||||||
@ -126,16 +118,13 @@ impl DepotRepository for MyStaticRepository {
|
|||||||
if let Some(index) = item_index {
|
if let Some(index) = item_index {
|
||||||
let item = &mut self.content[index];
|
let item = &mut self.content[index];
|
||||||
*item = data.clone();
|
*item = data.clone();
|
||||||
item["id"] = (*id).into();
|
|
||||||
|
|
||||||
if let Some(item_id) = item.get("id") {
|
if let Some(item_id) = item.get("id") {
|
||||||
return Ok(RepositoryResponse::ItemOnly(
|
return Some(model.build_item(&*item_id.to_string(), data));
|
||||||
model.build_item(&*item_id.to_string(), data),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(RepositoryResponse::NoItem)
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn replace(
|
async fn replace(
|
||||||
@ -143,7 +132,7 @@ impl DepotRepository for MyStaticRepository {
|
|||||||
model: &RepositoryContext,
|
model: &RepositoryContext,
|
||||||
id: &Self::Key,
|
id: &Self::Key,
|
||||||
data: Value,
|
data: Value,
|
||||||
) -> RepositoryResult {
|
) -> Option<RepositoryItem> {
|
||||||
self.update(model, id, data).await
|
self.update(model, id, data).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,11 +156,11 @@ impl DepotRepository for MyStaticRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(registry: &mut DepotRegistry) {
|
pub fn register(registry: &mut AdminRegistry) {
|
||||||
let section_key = registry.register_section("Example App");
|
let app_key = registry.register_app("Example App");
|
||||||
let repo = MyStaticRepository::new();
|
let repo = MyStaticRepository::new();
|
||||||
let model_config = DepotModelConfig {
|
let model_config = AdminModelConfig {
|
||||||
section_key: section_key,
|
app_key: app_key,
|
||||||
name: "ExampleModel".to_owned(),
|
name: "ExampleModel".to_owned(),
|
||||||
};
|
};
|
||||||
let model_result = registry.register_model(model_config, repo);
|
let model_result = registry.register_model(model_config, repo);
|
||||||
|
172
src/admin_examples/user_repository.rs
Normal file
172
src/admin_examples/user_repository.rs
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
use crate::admin::domain::*;
|
||||||
|
use crate::admin::state::AdminRegistry;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use log::debug;
|
||||||
|
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
struct UserRepository {
|
||||||
|
connection: DatabaseConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserRepository {
|
||||||
|
pub fn new(connection: DatabaseConnection) -> Self {
|
||||||
|
UserRepository {
|
||||||
|
connection: connection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AdminRepository for UserRepository {
|
||||||
|
type Key = i64;
|
||||||
|
|
||||||
|
fn key_from_string(&self, s: String) -> Option<Self::Key> {
|
||||||
|
if let Ok(i) = s.parse::<i64>() {
|
||||||
|
Some(i)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn info(&self, _: &RepositoryContext) -> RepositoryInfo {
|
||||||
|
RepoInfo {
|
||||||
|
name: "User",
|
||||||
|
lookup_key: "id",
|
||||||
|
display_list: &["id", "username"],
|
||||||
|
fields: &["username", "description"],
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> {
|
||||||
|
let id: i32 = *id as i32; // use try_into() instead.
|
||||||
|
let get_user = entity::User::find_by_id(id).one(&self.connection).await;
|
||||||
|
match get_user {
|
||||||
|
Ok(get_user) => {
|
||||||
|
if let Some(user) = get_user {
|
||||||
|
let id = user.id.to_string();
|
||||||
|
match serde_json::to_value(&user) {
|
||||||
|
Ok(item) => {
|
||||||
|
return Some(model.build_item(&*id, item));
|
||||||
|
}
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
|
||||||
|
let results = if let Ok(results) = entity::User::find().all(&self.connection).await {
|
||||||
|
results
|
||||||
|
} else {
|
||||||
|
return RepositoryList::Empty;
|
||||||
|
};
|
||||||
|
let repository_items: Vec<RepositoryItem> = results
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|item| match serde_json::to_value(&item) {
|
||||||
|
Ok(fields) => {
|
||||||
|
let id = item.id.to_string();
|
||||||
|
Some(model.build_item(&*id, fields))
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
RepositoryList::List {
|
||||||
|
values: repository_items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&mut self, model: &RepositoryContext, data: Value) -> Option<RepositoryItem> {
|
||||||
|
let username = data.get("username").unwrap().as_str().unwrap();
|
||||||
|
|
||||||
|
let mut user = entity::user::ActiveModel {
|
||||||
|
username: Set(username.to_owned()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(value) = data.get("description") {
|
||||||
|
user.description = Set(value.as_str().map(|s| s.to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(user) = user.insert(&self.connection).await {
|
||||||
|
let id = user.id.to_string();
|
||||||
|
return Some(model.build_item(&*id, serde_json::to_value(&user).unwrap()));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
model: &RepositoryContext,
|
||||||
|
id: &Self::Key,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
let id: i32 = *id as i32;
|
||||||
|
let user: Option<entity::user::Model> = entity::User::find_by_id(id)
|
||||||
|
.one(&self.connection)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut user: entity::user::ActiveModel = user.unwrap().into();
|
||||||
|
|
||||||
|
// change values
|
||||||
|
if let Some(value) = data.get("username") {
|
||||||
|
if let Some(value) = value.as_str() {
|
||||||
|
user.username = Set(value.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = data.get("description") {
|
||||||
|
user.description = Set(value.as_str().map(|s| s.to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// update
|
||||||
|
if let Ok(user) = user.update(&self.connection).await {
|
||||||
|
let id = user.id.to_string();
|
||||||
|
return Some(model.build_item(&*id, serde_json::to_value(&user).unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn replace(
|
||||||
|
&mut self,
|
||||||
|
model: &RepositoryContext,
|
||||||
|
id: &Self::Key,
|
||||||
|
data: Value,
|
||||||
|
) -> Option<RepositoryItem> {
|
||||||
|
self.update(model, id, data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&mut self, _: &RepositoryContext, id: &Self::Key) -> Option<Value> {
|
||||||
|
let id: i32 = *id as i32;
|
||||||
|
let user: Option<entity::user::Model> = entity::User::find_by_id(id)
|
||||||
|
.one(&self.connection)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if let Some(user) = user {
|
||||||
|
let delete_result = user.delete(&self.connection).await.unwrap();
|
||||||
|
debug!("deleted rows: {}", delete_result.rows_affected);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(registry: &mut AdminRegistry, db: DatabaseConnection) {
|
||||||
|
let app_key = registry.register_app("Auth");
|
||||||
|
let repo = UserRepository::new(db);
|
||||||
|
let model_config = AdminModelConfig {
|
||||||
|
app_key: app_key,
|
||||||
|
name: "User".to_owned(),
|
||||||
|
};
|
||||||
|
let model_result = registry.register_model(model_config, repo);
|
||||||
|
match model_result {
|
||||||
|
Err(err) => panic!("{}", err),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use rear_auth::models::UserRepository;
|
use entity;
|
||||||
use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, DbConn, DbErr, Set};
|
use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, DbConn, DbErr, Set};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::io::stdin;
|
use std::io::stdin;
|
||||||
@ -11,12 +11,11 @@ pub async fn establish_connection() -> DatabaseConnection {
|
|||||||
db.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
db.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_user(
|
pub async fn create_user(conn: &DbConn, username: &str) -> Result<entity::user::Model, DbErr> {
|
||||||
conn: &DbConn,
|
let user = entity::user::ActiveModel {
|
||||||
username: &str,
|
username: Set(username.to_owned()),
|
||||||
password: &str,
|
..Default::default()
|
||||||
) -> Result<entity::user::Model, DbErr> {
|
};
|
||||||
let user = UserRepository::new_user(username, password);
|
|
||||||
let user = user.insert(conn).await?;
|
let user = user.insert(conn).await?;
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
@ -26,16 +25,12 @@ async fn main() {
|
|||||||
let connection = establish_connection().await;
|
let connection = establish_connection().await;
|
||||||
|
|
||||||
let mut username = String::new();
|
let mut username = String::new();
|
||||||
let mut password = String::new();
|
|
||||||
|
|
||||||
println!("Enter username for new user:");
|
println!("Enter username for new user:");
|
||||||
stdin().read_line(&mut username).unwrap();
|
stdin().read_line(&mut username).unwrap();
|
||||||
let username = username.trim_end(); // Remove the trailing newline
|
let username = username.trim_end(); // Remove the trailing newline
|
||||||
println!("Enter password for new user:");
|
|
||||||
stdin().read_line(&mut password).unwrap();
|
|
||||||
let password = password.trim_end(); // Remove the trailing newline
|
|
||||||
|
|
||||||
let user = create_user(&connection, username, password)
|
let user = create_user(&connection, username)
|
||||||
.await
|
.await
|
||||||
.expect("Error creating user");
|
.expect("Error creating user");
|
||||||
println!("\nSaved user {} with id {}", username, user.id);
|
println!("\nSaved user {} with id {}", username, user.id);
|
||||||
|
@ -21,7 +21,7 @@ async fn main() {
|
|||||||
|
|
||||||
println!("Displaying {} users", results.len());
|
println!("Displaying {} users", results.len());
|
||||||
for user in results {
|
for user in results {
|
||||||
println!("{} - {}", user.username, user.password);
|
println!("{}", user.username);
|
||||||
}
|
}
|
||||||
println!("-----------\n");
|
println!("-----------\n");
|
||||||
}
|
}
|
||||||
|
32
src/main.rs
32
src/main.rs
@ -5,8 +5,9 @@ mod howto;
|
|||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use admin_examples::file_repository;
|
|
||||||
use admin_examples::static_repository;
|
use admin_examples::static_repository;
|
||||||
|
use admin_examples::user_repository;
|
||||||
|
use admin_examples::file_repository;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Bytes,
|
body::Bytes,
|
||||||
extract::MatchedPath,
|
extract::MatchedPath,
|
||||||
@ -16,14 +17,9 @@ use axum::{
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router,
|
extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router,
|
||||||
};
|
};
|
||||||
use axum_login::login_required;
|
|
||||||
use axum_login::tower_sessions::MemoryStore;
|
|
||||||
use axum_login::tower_sessions::SessionManagerLayer;
|
|
||||||
use axum_login::AuthManagerLayerBuilder;
|
|
||||||
use axum_messages::MessagesManagerLayer;
|
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use log::info;
|
use log::info;
|
||||||
use rear::depot;
|
use rear::admin;
|
||||||
use rear::service::{handlers, templates};
|
use rear::service::{handlers, templates};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
@ -53,11 +49,11 @@ async fn main() {
|
|||||||
|
|
||||||
// Prepare Application State Members
|
// Prepare Application State Members
|
||||||
let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded.");
|
let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded.");
|
||||||
let mut admin = depot::prelude::DepotRegistry::new("admin");
|
let mut admin = admin::state::AdminRegistry::new("admin");
|
||||||
|
|
||||||
// Register Admin Apps
|
// Register Admin Apps
|
||||||
static_repository::register(&mut admin);
|
static_repository::register(&mut admin);
|
||||||
let admin_user_repo = rear_auth::user_admin_repository::register(&mut admin, db_connection);
|
user_repository::register(&mut admin, db_connection);
|
||||||
file_repository::register(&mut admin, "static/admin");
|
file_repository::register(&mut admin, "static/admin");
|
||||||
|
|
||||||
// Create global Application State.
|
// Create global Application State.
|
||||||
@ -66,28 +62,12 @@ async fn main() {
|
|||||||
admin: Arc::new(admin),
|
admin: Arc::new(admin),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Session System
|
|
||||||
let session_store = MemoryStore::default();
|
|
||||||
let session_layer = SessionManagerLayer::new(session_store);
|
|
||||||
// Auth service.
|
|
||||||
let auth_layer = AuthManagerLayerBuilder::new(admin_user_repo, session_layer).build();
|
|
||||||
|
|
||||||
// Application Route
|
// Application Route
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(home))
|
.route("/", get(home))
|
||||||
.route("/hello", get(hello_world))
|
.route("/hello", get(hello_world))
|
||||||
//.merge(admin_router)
|
//.merge(admin_router)
|
||||||
.nest(
|
.nest("/admin", admin::routes())
|
||||||
"/admin",
|
|
||||||
depot::routes()
|
|
||||||
.route_layer(login_required!(
|
|
||||||
rear_auth::models::UserRepository,
|
|
||||||
login_url = "/admin/login"
|
|
||||||
))
|
|
||||||
.merge(rear_auth::views::routes())
|
|
||||||
.layer(MessagesManagerLayer)
|
|
||||||
.layer(auth_layer),
|
|
||||||
)
|
|
||||||
.nest("/howto", howto::routes())
|
.nest("/howto", howto::routes())
|
||||||
.route_service("/static/*file", embed::static_handler.into_service())
|
.route_service("/static/*file", embed::static_handler.into_service())
|
||||||
.fallback(handlers::not_found_handler)
|
.fallback(handlers::not_found_handler)
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use axum::extract::FromRef;
|
use axum::extract::FromRef;
|
||||||
|
|
||||||
use rear::depot::state::{DepotState, SharedDepotRegistry};
|
use rear::admin::state::{AdminState, SharedAdminRegistry};
|
||||||
use rear::service::templates;
|
use rear::service::templates;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub templates: templates::Templates,
|
pub templates: templates::Templates,
|
||||||
pub admin: SharedDepotRegistry,
|
pub admin: SharedAdminRegistry,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromRef<AppState> for templates::Templates {
|
impl FromRef<AppState> for templates::Templates {
|
||||||
@ -15,12 +15,12 @@ impl FromRef<AppState> for templates::Templates {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DepotState for AppState {
|
impl AdminState for AppState {
|
||||||
fn get_templates(&self) -> &templates::Templates {
|
fn get_templates(&self) -> &templates::Templates {
|
||||||
&self.templates
|
&self.templates
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_registry(&self) -> SharedDepotRegistry {
|
fn get_registry(&self) -> SharedAdminRegistry {
|
||||||
self.admin.clone()
|
self.admin.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
2
static/htmx/htmx.min.js
vendored
2
static/htmx/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
2
static/hyperscript/hyperscript.min.js
vendored
2
static/hyperscript/hyperscript.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
https://unpkg.com/hyperscript.org@0.9.12/dist/_hyperscript.min.js
|
https://unpkg.com/hyperscript.org@0.9.11/dist/_hyperscript.min.js
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
{% if sections %}
|
{% if app_list %}
|
||||||
{% for section in sections %}
|
{% for app in app_list %}
|
||||||
<div
|
<div
|
||||||
class="section-{{ section.key }} module{% if section.section_url in request.path|urlencode %} current-section{% endif %} ui short scrolling container">
|
class="app-{{ app.key }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %} ui short scrolling container">
|
||||||
<table class="ui very compact celled table head stuck unstackable">
|
<table class="ui very compact celled table head stuck unstackable">
|
||||||
<caption>
|
<caption>
|
||||||
<a href="{{ section.section_url }}" class="section"
|
<a href="{{ app.app_url }}" class="section" title="Models in the {{ name }} application">{{ app.name }}</a>
|
||||||
title="Models in the {{ name }} Section">{{ section.name }}</a>
|
|
||||||
</caption>
|
</caption>
|
||||||
{% for model in section.models %}
|
{% for model in app.models %}
|
||||||
<tr class="model-{{ model.key }}{% if model.model_url in request.path|urlencode %} current-model{% endif %}">
|
<tr class="model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
||||||
{% if model.model_url %}
|
{% if model.admin_url %}
|
||||||
<th scope="row"><a href="{{ model.model_url }}" {% if model.model_url in request.path|urlencode %}
|
<th scope="row"><a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %}
|
||||||
aria-current="page" {% endif %}>{{ model.name }}</a></th>
|
aria-current="page" {% endif %}>{{ model.name }}</a></th>
|
||||||
{% else %}
|
{% else %}
|
||||||
<th scope="row">{{ model.name }}</th>
|
<th scope="row">{{ model.name }}</th>
|
||||||
@ -22,11 +21,11 @@
|
|||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if model.model_url and show_changelinks %}
|
{% if model.admin_url and show_changelinks %}
|
||||||
{% if model.view_only %}
|
{% if model.view_only %}
|
||||||
<td><a href="{{ model.model_url }}" class="viewlink">{{ translate( 'View') }}</a></td>
|
<td><a href="{{ model.admin_url }}" class="viewlink">{{ translate( 'View') }}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td><a href="{{ model.model_url }}" class="changelink">{{ translate( 'Change') }}</a></td>
|
<td><a href="{{ model.admin_url }}" class="changelink">{{ translate( 'Change') }}</a></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif show_changelinks %}
|
{% elif show_changelinks %}
|
||||||
<td></td>
|
<td></td>
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
{% include "fomantic.html" %}
|
{% include "fomantic.html" %}
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/rear/depot.css">
|
<link rel="stylesheet" type="text/css" href="/static/admin/admin.css">
|
||||||
|
|
||||||
{% block extrastyle %}{% endblock %}
|
{% block extrastyle %}{% endblock %}
|
||||||
{% block extrahead %}{% endblock %}
|
{% block extrahead %}{% endblock %}
|
||||||
@ -42,14 +42,14 @@
|
|||||||
{% endblock usertools %}
|
{% endblock usertools %}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<a class="ui blue image label">
|
<a class="ui blue image label">
|
||||||
<img src="/static/rear/teddy_bear.png">
|
<img src="/static/admin/teddy_bear.png">
|
||||||
Person
|
Person
|
||||||
<div class="detail">Admiral</div>
|
<div class="detail">Admin</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="icon item">
|
<div class="icon item">
|
||||||
<form id="logout-form" method="post" action="{{ url('depot:logout') }}">
|
<form id="logout-form" method="post" action="{{ url('admin:logout') }}">
|
||||||
<i class="power link icon" style="float: none;"></i>
|
<i class="power link icon" style="float: none;"></i>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -62,7 +62,7 @@
|
|||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
<div class="ui vertical sidebar left visible overlay" id="main_sidemenu">
|
<div class="ui vertical sidebar left visible overlay" id="main_sidemenu">
|
||||||
<div class="ui vertical inverted fluid menu">
|
<div class="ui vertical inverted fluid menu">
|
||||||
<div class="item"><a href="{{depot_url}}">
|
<div class="item"><a href="{{admin_url}}">
|
||||||
<i class="big d20 dice icon" style="float: none;"></i>
|
<i class="big d20 dice icon" style="float: none;"></i>
|
||||||
<b>Administration</b>
|
<b>Administration</b>
|
||||||
</a>
|
</a>
|
||||||
@ -97,17 +97,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if sections %}
|
{% if available_apps %}
|
||||||
{% for section in sections %}
|
{% for app in available_apps %}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<i class="cog link icon" href="{{ section.section_url }}"></i>
|
<i class="cog link icon" href="{{ app.admin_url }}"></i>
|
||||||
<div class="header">{{ section.name }} </div>
|
<div class="header">{{ app.name }} </div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
{% for model in section.models %}
|
{% for model in app.models %}
|
||||||
<div
|
<div
|
||||||
class="item model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
class="item model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
||||||
{% if model.model_url %}
|
{% if model.admin_url %}
|
||||||
<a href="{{ model.model_url }}" hx-get="{{ model.model_url }}" hx-target="#main" hx-push-url="true" {% if
|
<a href="{{ model.admin_url }}" hx-get="{{ model.admin_url }}" hx-target="#main" hx-push-url="true" {% if
|
||||||
model.admin_url in request.path|urlencode %} aria-current="page" {% endif %}>{{ model.name }}</a>
|
model.admin_url in request.path|urlencode %} aria-current="page" {% endif %}>{{ model.name }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span>{{ model.name }}</span>
|
<span>{{ model.name }}</span>
|
||||||
@ -139,9 +139,6 @@
|
|||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
{% block bodyjs %}
|
{% block bodyjs %}
|
||||||
<script src="/static/htmx/htmx.min.js"></script>
|
<script src="/static/htmx/htmx.min.js"></script>
|
||||||
<script src="/static/hyperscript/hyperscript.min.js"></script>
|
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$('#main_sidemenu').sidebar({
|
$('#main_sidemenu').sidebar({
|
10
templates/admin/index.html
Normal file
10
templates/admin/index.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "admin/base.jinja" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include "admin/dashboard.jinja" %}
|
||||||
|
<div>
|
||||||
|
Some text
|
||||||
|
another text
|
||||||
|
Third text
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
10
templates/admin/items/item_change.jinja
Normal file
10
templates/admin/items/item_change.jinja
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% extends base|none("admin/base.jinja") %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="ui container">
|
||||||
|
<h1>Update {{item_model.name}} in {{item_info.name}}</h1>
|
||||||
|
{% set fields = item_info.fields %}
|
||||||
|
{% set form = { 'action': item.change_url } %}
|
||||||
|
{% include "/admin/items/item_change_form.jinja" %}
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
16
templates/admin/items/item_change_form.jinja
Normal file
16
templates/admin/items/item_change_form.jinja
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<form action="{{form.action}}" method="{{form.method|default('POST')}}" class="ui form">
|
||||||
|
<!--noformat-->
|
||||||
|
{% from "/admin/items/items.jinja" import field_widget %}
|
||||||
|
{% for field_name, field_defs in fields %}
|
||||||
|
{% if item %}
|
||||||
|
{% set field_value = item.fields[field_name]|none("") %}
|
||||||
|
{% else %}
|
||||||
|
{% set field_value = "" %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="field">
|
||||||
|
{{ field_widget(field_name, field_defs, field_value) }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<!--noformat-->
|
||||||
|
<button class="ui button" type="submit">Create</button>
|
||||||
|
</form>
|
@ -1,4 +1,4 @@
|
|||||||
{% extends base|none("depot/base.jinja") %}
|
{% extends base|none("admin/base.jinja") %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
@ -12,6 +12,6 @@
|
|||||||
|
|
||||||
{% set fields = item_info.fields %}
|
{% set fields = item_info.fields %}
|
||||||
{% set form = { 'action': item_model.add_url } %}
|
{% set form = { 'action': item_model.add_url } %}
|
||||||
{% include "/depot/items/item_change_form.jinja" %}
|
{% include "/admin/items/item_change_form.jinja" %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
11
templates/admin/items/item_detail.jinja
Normal file
11
templates/admin/items/item_detail.jinja
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends base|none("admin/base.jinja") %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% if item %}
|
||||||
|
{{item.fields}}
|
||||||
|
{% else %}
|
||||||
|
No Item found.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock content %}
|
@ -1,4 +1,4 @@
|
|||||||
{% extends base|none("depot/base.jinja") %}
|
{% extends base|none("admin/base.jinja") %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui container">
|
<div class="ui container">
|
@ -1,10 +0,0 @@
|
|||||||
{% extends "depot/base.jinja" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% include "depot/dashboard.jinja" %}
|
|
||||||
<div>
|
|
||||||
Some text
|
|
||||||
another text
|
|
||||||
Third text
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,13 +0,0 @@
|
|||||||
{% extends base|none("depot/base.jinja") %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div id="message-box-area" class="ui message" _="init wait 3s then transition opacity to 0 then remove me">
|
|
||||||
Hi there.
|
|
||||||
</div>
|
|
||||||
<div class="ui container">
|
|
||||||
<h1>Update {{item_model.name}} in {{item_info.name}}</h1>
|
|
||||||
{% set fields = item_info.fields %}
|
|
||||||
{% set form = { 'action': item.change_url, 'method': 'PATCH' } %}
|
|
||||||
{% include "/depot/items/item_change_form.jinja" %}
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
@ -1,29 +0,0 @@
|
|||||||
<!--noformat-->
|
|
||||||
<form class="ui form"
|
|
||||||
hx-target="main"
|
|
||||||
{% if form.method|upper == 'POST' or not form.method %}
|
|
||||||
hx-post="{{ form.action }}"
|
|
||||||
{% elif form.method|upper == 'GET' %}
|
|
||||||
hx-get="{{ form.action }}"
|
|
||||||
{% elif form.method|upper == 'PATCH' %}
|
|
||||||
hx-patch="{{ form.action }}"
|
|
||||||
{% elif form.method|upper == 'PUT' %}
|
|
||||||
hx-put="{{ form.action }}"
|
|
||||||
{% else %}
|
|
||||||
unknown-form-method="{{ form.method }}"
|
|
||||||
{% endif %}>
|
|
||||||
|
|
||||||
{% from "/depot/items/items.jinja" import field_widget %}
|
|
||||||
{% for field_name, field_defs in fields %}
|
|
||||||
{% if item %}
|
|
||||||
{% set field_value = item.fields[field_name]|none("") %}
|
|
||||||
{% else %}
|
|
||||||
{% set field_value = "" %}
|
|
||||||
{% endif %}
|
|
||||||
<div class="field">
|
|
||||||
{{ field_widget(field_name, field_defs, field_value) }}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
<button class="ui button" type="submit">Create</button>
|
|
||||||
</form>
|
|
||||||
<!--noformat-->
|
|
@ -1,11 +0,0 @@
|
|||||||
{% extends base|none("depot/base.jinja") %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
{% if item %}
|
|
||||||
{{item.fields}}
|
|
||||||
{% else %}
|
|
||||||
No Item found.
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock content %}
|
|
@ -1,43 +0,0 @@
|
|||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>Login</title>
|
|
||||||
<style>
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<ul>
|
|
||||||
{% for message in messages %}
|
|
||||||
<li>
|
|
||||||
<span><strong>{{ message }}</strong></span>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<form method="post">
|
|
||||||
<fieldset>
|
|
||||||
<legend>User login</legend>
|
|
||||||
<p>
|
|
||||||
<label for="username">Username</label>
|
|
||||||
<input name="username" id="username" value="admin" />
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<label for="password">Password</label>
|
|
||||||
<input name="password" id="password" type="password" value="admin" />
|
|
||||||
</p>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<input type="submit" value="login" />
|
|
||||||
|
|
||||||
{% if next %}
|
|
||||||
<input type="hidden" name="next" value="{{next}}" />
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,8 +0,0 @@
|
|||||||
<div class="ui toggle checkbox">
|
|
||||||
<!--noformat-->
|
|
||||||
<input type="checkbox"
|
|
||||||
name="{{ field.name }}"
|
|
||||||
{% if field.value %}checked="checked" {% endif %}>
|
|
||||||
<label>{{field.label or field.name|capitalize}}</label>
|
|
||||||
<!--noformat-->
|
|
||||||
</div>
|
|
@ -1,8 +0,0 @@
|
|||||||
<div class="ui action input inline field">
|
|
||||||
<label>{{field.label or field.name|capitalize}}</label>
|
|
||||||
<input type="text" value="{{field.value}}" readonly="readonly">
|
|
||||||
<button class="ui teal right labeled icon button" hx-disable>
|
|
||||||
<i class="key icon"></i>
|
|
||||||
Set Password...
|
|
||||||
</button>
|
|
||||||
</div>
|
|
Loading…
Reference in New Issue
Block a user