This commit is contained in:
parent
d4448c6129
commit
d29679c6f8
365
Cargo.lock
generated
365
Cargo.lock
generated
@ -45,6 +45,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
@ -870,6 +871,31 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "casbin"
|
||||
version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a100183440478aa2b64e6f432295fa90c052d53011c9799b655b7283b6e6707c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"fixedbitset",
|
||||
"getrandom 0.2.16",
|
||||
"hashlink 0.9.1",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"petgraph",
|
||||
"regex",
|
||||
"rhai",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slog",
|
||||
"slog-async",
|
||||
"slog-term",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"wasm-bindgen-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.30"
|
||||
@ -1008,6 +1034,26 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@ -1058,6 +1104,15 @@ version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
@ -1073,6 +1128,12 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@ -1201,6 +1262,27 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-next"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dirs-sys-next",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys-next"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
@ -1336,6 +1418,12 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
@ -1344,7 +1432,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"spin",
|
||||
"spin 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1653,6 +1741,15 @@ dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.10.0"
|
||||
@ -2089,6 +2186,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.2",
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
@ -2160,7 +2268,7 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
dependencies = [
|
||||
"spin",
|
||||
"spin 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2216,6 +2324,16 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.30.1"
|
||||
@ -2310,6 +2428,7 @@ dependencies = [
|
||||
"axum-jwt-auth",
|
||||
"axum-reverse-proxy",
|
||||
"axum_gcra",
|
||||
"casbin",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dotenvy",
|
||||
@ -2323,6 +2442,8 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"sqlx-adapter",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tower 0.4.13",
|
||||
"tower-http 0.5.2",
|
||||
@ -2374,6 +2495,16 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minicov"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@ -2413,7 +2544,7 @@ dependencies = [
|
||||
"httparse",
|
||||
"memchr",
|
||||
"mime",
|
||||
"spin",
|
||||
"spin 0.9.8",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
@ -2447,6 +2578,15 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||
dependencies = [
|
||||
"spin 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@ -2575,6 +2715,9 @@ name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
@ -2741,6 +2884,16 @@ dependencies = [
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
@ -2821,6 +2974,12 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
@ -3082,6 +3241,17 @@ dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
@ -3158,6 +3328,36 @@ dependencies = [
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.22.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2780e813b755850e50b178931aaf94ed24f6817f46aaaf5d21c13c12d939a249"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"bitflags 2.9.1",
|
||||
"instant",
|
||||
"no-std-compat",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rhai_codegen",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"thin-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_codegen"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@ -3357,6 +3557,15 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scc"
|
||||
version = "2.3.4"
|
||||
@ -3613,6 +3822,37 @@ version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
|
||||
|
||||
[[package]]
|
||||
name = "slog"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06"
|
||||
|
||||
[[package]]
|
||||
name = "slog-async"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72c8038f898a2c79507940990f05386455b3a317d8f18d4caea7cbc3d5096b84"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"slog",
|
||||
"take_mut",
|
||||
"thread_local",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slog-term"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8"
|
||||
dependencies = [
|
||||
"is-terminal",
|
||||
"slog",
|
||||
"term",
|
||||
"thread_local",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
@ -3622,6 +3862,18 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.10"
|
||||
@ -3652,6 +3904,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@ -3684,6 +3942,18 @@ dependencies = [
|
||||
"sqlx-sqlite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-adapter"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a88e13f5aaf770420184c9e2955345f157953fb7ed9f26df59a4a0664478daf"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"casbin",
|
||||
"dotenvy",
|
||||
"sqlx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.8.6"
|
||||
@ -3705,7 +3975,7 @@ dependencies = [
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hashbrown 0.15.4",
|
||||
"hashlink",
|
||||
"hashlink 0.10.0",
|
||||
"indexmap",
|
||||
"ipnetwork",
|
||||
"log",
|
||||
@ -3721,6 +3991,8 @@ dependencies = [
|
||||
"smallvec",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
@ -3761,6 +4033,7 @@ dependencies = [
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"syn 2.0.104",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -3902,6 +4175,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions_next"
|
||||
version = "1.1.2"
|
||||
@ -3995,6 +4274,12 @@ dependencies = [
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "take_mut"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
@ -4014,6 +4299,26 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
|
||||
dependencies = [
|
||||
"dirs-next",
|
||||
"rustversion",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@ -4094,6 +4399,15 @@ dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.1"
|
||||
@ -4519,6 +4833,16 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
@ -4620,6 +4944,30 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"minicov",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-bindgen-test-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.77"
|
||||
@ -4687,6 +5035,15 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
|
||||
@ -51,4 +51,8 @@ rdkafka = "0.38.0"
|
||||
axum_gcra = "0.1.1"
|
||||
lettre = "0.11.18"
|
||||
anyhow = "1.0.98"
|
||||
thiserror = "2.0.12"
|
||||
casbin = { version = "2.0", features = ["logging", "incremental","runtime-tokio"] }
|
||||
sqlx-adapter = { version = "1.8.0", default-features = false, features = ["postgres", "runtime-tokio-native-tls"]}
|
||||
|
||||
|
||||
|
||||
244
README.md
244
README.md
@ -1,119 +1,201 @@
|
||||
# 瓦片服务器 (Tile Server)
|
||||
# Map Application Server (MAPP)
|
||||
|
||||
基于 Axum 的高性能瓦片服务器,支持通过时间参数动态加载不同时次的瓦片数据。
|
||||
一个基于 GraphQL 的现代地图应用服务器,支持实时数据同步、权限管理和多租户架构。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 支持时间参数动态加载瓦片
|
||||
- ✅ 标准 XYZ 瓦片格式支持
|
||||
- ✅ 高性能异步文件读取
|
||||
- ✅ 适当的 HTTP 缓存头设置
|
||||
- ✅ 完整的错误处理
|
||||
- ✅ CORS 支持
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
tiles/
|
||||
└── 202507220000/ ← 时次目录(YYYYMMDDHHMM)
|
||||
└── {z}/{x}/{y}.png
|
||||
└── 202507220006/
|
||||
└── {z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 获取瓦片
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /tiles/{z}/{x}/{y}.png?time={time}
|
||||
```
|
||||
|
||||
**参数说明**
|
||||
- `z`: 缩放级别 (u8)
|
||||
- `x`: X 坐标 (u32)
|
||||
- `y`: Y 坐标 (u32)
|
||||
- `time`: 时间参数,格式为 YYYYMMDDHHMM (12位数字)
|
||||
|
||||
**响应**
|
||||
- `200 OK` - 返回 PNG 图像数据
|
||||
- `400 Bad Request` - 参数错误
|
||||
- `404 Not Found` - 瓦片文件不存在
|
||||
|
||||
**示例请求**
|
||||
```bash
|
||||
curl "http://localhost:3050/tiles/6/42/20.png?time=202507220006"
|
||||
```
|
||||
- 🗺️ 基于 GraphQL 的地图数据服务
|
||||
- 🔐 基于 Casbin 的 RBAC 权限管理
|
||||
- 📡 Kafka 消息队列集成
|
||||
- 🗄️ PostgreSQL 数据库支持
|
||||
- 🚀 高性能异步架构
|
||||
- 🔧 完整的 CLI 管理工具
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 编译运行
|
||||
### 环境要求
|
||||
|
||||
- Rust 1.70+
|
||||
- PostgreSQL 12+
|
||||
- Kafka 2.8+ (可选)
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
cargo run
|
||||
# 克隆项目
|
||||
git clone <repository-url>
|
||||
cd mapp
|
||||
|
||||
# 安装依赖
|
||||
cargo build --release
|
||||
|
||||
# 设置环境变量
|
||||
export DATABASE_URL="postgresql://username:password@localhost/mapp"
|
||||
export JWT_SECRET="your-secret-key"
|
||||
export PORT=3000
|
||||
```
|
||||
|
||||
### 2. 测试 API
|
||||
### 运行
|
||||
|
||||
访问根路径查看服务状态:
|
||||
```bash
|
||||
curl http://localhost:3050/
|
||||
# 启动服务器
|
||||
./target/release/mapp serve
|
||||
|
||||
# 开发模式
|
||||
./target/release/mapp serve --dev --verbose
|
||||
|
||||
# 运行数据库迁移
|
||||
./target/release/mapp migrate
|
||||
```
|
||||
|
||||
获取示例瓦片:
|
||||
## 权限管理 CLI
|
||||
|
||||
MAPP 提供了完整的命令行权限管理工具,基于 Casbin RBAC 模型:
|
||||
|
||||
### 基本用法
|
||||
|
||||
```bash
|
||||
curl "http://localhost:3050/tiles/6/42/20.png?time=202507220006"
|
||||
# 查看权限管理帮助
|
||||
./target/release/mapp permissions --help
|
||||
|
||||
# 列出所有权限策略
|
||||
./target/release/mapp permissions list
|
||||
|
||||
# 重新加载权限策略
|
||||
./target/release/mapp permissions reload
|
||||
```
|
||||
|
||||
### 3. 创建瓦片数据
|
||||
|
||||
按照以下目录结构组织您的瓦片文件:
|
||||
### 权限策略管理
|
||||
|
||||
```bash
|
||||
mkdir -p tiles/202507220000/6/42
|
||||
# 将您的瓦片文件放入相应目录
|
||||
cp your-tile.png tiles/202507220000/6/42/20.png
|
||||
# 添加权限策略:给 admin 角色添加删除用户的权限
|
||||
./target/release/mapp permissions add --role admin --resource users --action delete
|
||||
|
||||
# 移除权限策略:移除 admin 角色的删除用户权限
|
||||
./target/release/mapp permissions remove --role admin --resource users --action delete
|
||||
|
||||
# 查看角色权限
|
||||
./target/release/mapp permissions list-role-permissions --role admin
|
||||
```
|
||||
|
||||
### 用户角色管理
|
||||
|
||||
```bash
|
||||
# 为用户分配角色
|
||||
./target/release/mapp permissions assign-role --user-id user123 --role editor
|
||||
|
||||
# 移除用户角色
|
||||
./target/release/mapp permissions remove-role --user-id user123 --role editor
|
||||
|
||||
# 查看用户角色
|
||||
./target/release/mapp permissions list-user-roles --user-id user123
|
||||
```
|
||||
|
||||
### 权限检查
|
||||
|
||||
```bash
|
||||
# 检查用户是否有特定权限
|
||||
./target/release/mapp permissions check --user-id user123 --resource pages --action write
|
||||
```
|
||||
|
||||
### 常用权限配置示例
|
||||
|
||||
```bash
|
||||
# 创建管理员角色权限
|
||||
./target/release/mapp permissions add --role admin --resource "*" --action "*"
|
||||
|
||||
# 创建编辑者角色权限
|
||||
./target/release/mapp permissions add --role editor --resource pages --action read
|
||||
./target/release/mapp permissions add --role editor --resource pages --action write
|
||||
./target/release/mapp permissions add --role editor --resource page_blocks --action read
|
||||
./target/release/mapp permissions add --role editor --resource page_blocks --action write
|
||||
|
||||
# 创建查看者角色权限
|
||||
./target/release/mapp permissions add --role viewer --resource pages --action read
|
||||
./target/release/mapp permissions add --role viewer --resource page_blocks --action read
|
||||
|
||||
# 为用户分配角色
|
||||
./target/release/mapp permissions assign-role --user-id admin@example.com --role admin
|
||||
./target/release/mapp permissions assign-role --user-id editor@example.com --role editor
|
||||
./target/release/mapp permissions assign-role --user-id viewer@example.com --role viewer
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
- **端口**: 3050 (可在 main.rs 中修改)
|
||||
- **绑定地址**: 0.0.0.0 (监听所有网络接口)
|
||||
- **缓存策略**: 1小时 (public, max-age=3600)
|
||||
### 环境变量
|
||||
|
||||
## 错误处理
|
||||
| 变量名 | 描述 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `DATABASE_URL` | PostgreSQL 连接字符串 | 必需 |
|
||||
| `JWT_SECRET` | JWT 签名密钥 | 必需 |
|
||||
| `PORT` | 服务器端口 | 3000 |
|
||||
| `TILE_SERVER` | 瓦片服务器 URL | 必需 |
|
||||
| `KAFKA_BROKERS` | Kafka 集群地址 | localhost:9092 |
|
||||
| `KAFKA_TOPIC` | Kafka 主题 | mapp-events |
|
||||
| `KAFKA_GROUP_ID` | Kafka 消费者组 | mapp-group |
|
||||
|
||||
服务器会返回详细的错误信息:
|
||||
### 数据库迁移
|
||||
|
||||
- 缺少时间参数: `400 Bad Request`
|
||||
- 时间格式无效: `400 Bad Request`
|
||||
- 瓦片不存在: `404 Not Found`
|
||||
- 服务器错误: `500 Internal Server Error`
|
||||
```bash
|
||||
# 运行所有迁移
|
||||
./target/release/mapp migrate
|
||||
|
||||
## 开发说明
|
||||
# 查看迁移状态
|
||||
./target/release/mapp migrate --dry-run
|
||||
|
||||
# 强制重新运行迁移
|
||||
./target/release/mapp migrate --force
|
||||
```
|
||||
|
||||
## 开发
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
└── main.rs # 主服务器代码
|
||||
Cargo.toml # 依赖配置
|
||||
README.md # 项目说明
|
||||
tiles/ # 瓦片数据目录
|
||||
├── app.rs # 应用路由配置
|
||||
├── auth.rs # 认证和授权
|
||||
├── cli.rs # 命令行接口
|
||||
├── config.rs # 配置管理
|
||||
├── db.rs # 数据库连接
|
||||
├── graphql/ # GraphQL 相关
|
||||
│ ├── guards.rs # 权限守卫
|
||||
│ ├── mutation.rs # 变更操作
|
||||
│ ├── query.rs # 查询操作
|
||||
│ └── types.rs # 类型定义
|
||||
├── listener/ # 消息监听器
|
||||
├── models/ # 数据模型
|
||||
└── services/ # 业务服务
|
||||
└── casbin_service.rs # 权限管理服务
|
||||
```
|
||||
|
||||
### 核心依赖
|
||||
### 构建
|
||||
|
||||
- `axum`: Web 框架
|
||||
- `tokio`: 异步运行时
|
||||
- `tower-http`: HTTP 中间件 (CORS)
|
||||
- `tracing`: 日志记录
|
||||
```bash
|
||||
# 开发构建
|
||||
cargo build
|
||||
|
||||
### 扩展建议
|
||||
# 发布构建
|
||||
cargo build --release
|
||||
|
||||
- 添加瓦片格式验证
|
||||
- 实现瓦片缓存机制
|
||||
- 支持多种图像格式
|
||||
- 添加访问日志
|
||||
- 实现配置文件支持
|
||||
# 运行测试
|
||||
cargo test
|
||||
|
||||
# 代码检查
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件。
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 支持
|
||||
|
||||
如有问题,请通过以下方式联系:
|
||||
|
||||
- 提交 GitHub Issue
|
||||
- 发送邮件至 [support@example.com]
|
||||
158
migrations/007_create_page_blocks.sql
Normal file
158
migrations/007_create_page_blocks.sql
Normal file
@ -0,0 +1,158 @@
|
||||
-- 创建页面表
|
||||
CREATE TABLE IF NOT EXISTS pages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
created_by UUID REFERENCES users(id),
|
||||
updated_by UUID REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- 创建文本块表
|
||||
CREATE TABLE IF NOT EXISTS text_blocks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
page_id UUID NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
block_order INTEGER NOT NULL,
|
||||
title VARCHAR(255),
|
||||
markdown TEXT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(page_id, block_order)
|
||||
);
|
||||
|
||||
-- 创建图表块表
|
||||
CREATE TABLE IF NOT EXISTS chart_blocks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
page_id UUID NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
block_order INTEGER NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
chart_type VARCHAR(50) NOT NULL,
|
||||
config JSONB,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(page_id, block_order)
|
||||
);
|
||||
|
||||
-- 创建数据点表
|
||||
CREATE TABLE IF NOT EXISTS data_points (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
chart_block_id UUID NOT NULL REFERENCES chart_blocks(id) ON DELETE CASCADE,
|
||||
x DOUBLE PRECISION NOT NULL,
|
||||
y DOUBLE PRECISION NOT NULL,
|
||||
label VARCHAR(255),
|
||||
color VARCHAR(50),
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 创建设置块表
|
||||
CREATE TABLE IF NOT EXISTS settings_blocks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
page_id UUID NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
block_order INTEGER NOT NULL,
|
||||
title VARCHAR(255),
|
||||
category VARCHAR(100) NOT NULL,
|
||||
editable BOOLEAN NOT NULL DEFAULT true,
|
||||
display_mode VARCHAR(50) NOT NULL DEFAULT 'form',
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(page_id, block_order)
|
||||
);
|
||||
|
||||
-- 创建表格块表
|
||||
CREATE TABLE IF NOT EXISTS table_blocks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
page_id UUID NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
block_order INTEGER NOT NULL,
|
||||
title VARCHAR(255),
|
||||
data_source VARCHAR(100) NOT NULL,
|
||||
data_config JSONB,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(page_id, block_order)
|
||||
);
|
||||
|
||||
-- 创建表格列定义表
|
||||
CREATE TABLE IF NOT EXISTS table_columns (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
table_block_id UUID NOT NULL REFERENCES table_blocks(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
label VARCHAR(255) NOT NULL,
|
||||
data_type VARCHAR(50) NOT NULL,
|
||||
is_sortable BOOLEAN NOT NULL DEFAULT false,
|
||||
is_filterable BOOLEAN NOT NULL DEFAULT false,
|
||||
width INTEGER,
|
||||
"order" INTEGER NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(table_block_id, "order")
|
||||
);
|
||||
|
||||
-- 创建英雄块表
|
||||
CREATE TABLE IF NOT EXISTS hero_blocks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
page_id UUID NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
block_order INTEGER NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
subtitle TEXT,
|
||||
background_image VARCHAR(500),
|
||||
background_color VARCHAR(50),
|
||||
text_color VARCHAR(50),
|
||||
cta_text VARCHAR(100),
|
||||
cta_link VARCHAR(500),
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(page_id, block_order)
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_pages_slug ON pages(slug);
|
||||
CREATE INDEX IF NOT EXISTS idx_pages_is_active ON pages(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_text_blocks_page_id ON text_blocks(page_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_chart_blocks_page_id ON chart_blocks(page_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_settings_blocks_page_id ON settings_blocks(page_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_table_blocks_page_id ON table_blocks(page_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_hero_blocks_page_id ON hero_blocks(page_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_data_points_chart_block_id ON data_points(chart_block_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_table_columns_table_block_id ON table_columns(table_block_id);
|
||||
|
||||
-- 创建更新时间触发器函数
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- 为所有表添加更新时间触发器
|
||||
CREATE TRIGGER update_pages_updated_at BEFORE UPDATE ON pages
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_text_blocks_updated_at BEFORE UPDATE ON text_blocks
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_chart_blocks_updated_at BEFORE UPDATE ON chart_blocks
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_settings_blocks_updated_at BEFORE UPDATE ON settings_blocks
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_table_blocks_updated_at BEFORE UPDATE ON table_blocks
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_hero_blocks_updated_at BEFORE UPDATE ON hero_blocks
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 插入一些示例数据
|
||||
INSERT INTO pages (id, title, slug, description, is_active) VALUES
|
||||
(gen_random_uuid(), '系统设置', 'system-settings', '系统配置管理页面', true),
|
||||
(gen_random_uuid(), '数据概览', 'data-overview', '数据统计和图表展示', true),
|
||||
(gen_random_uuid(), '用户管理', 'user-management', '用户信息管理页面', true)
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
165
migrations/008_create_category_pages.sql
Normal file
165
migrations/008_create_category_pages.sql
Normal file
@ -0,0 +1,165 @@
|
||||
-- 为每个配置分类创建对应的页面和默认的排版块
|
||||
-- 这个迁移文件将创建以下页面:
|
||||
-- 1. app - 应用配置页面
|
||||
-- 2. database - 数据库配置页面
|
||||
-- 3. kafka - Kafka配置页面
|
||||
-- 4. security - 安全配置页面
|
||||
-- 5. logging - 日志配置页面
|
||||
-- 6. cache - 缓存配置页面
|
||||
|
||||
-- 创建应用配置页面
|
||||
INSERT INTO pages (id, title, slug, description, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), '应用配置', 'app-settings', '应用基本配置管理页面', true, NOW(), NOW()),
|
||||
(gen_random_uuid(), '数据库配置', 'database-settings', '数据库连接和性能配置管理页面', true, NOW(), NOW()),
|
||||
(gen_random_uuid(), 'Kafka配置', 'kafka-settings', 'Kafka消息队列配置管理页面', true, NOW(), NOW()),
|
||||
(gen_random_uuid(), '安全配置', 'security-settings', '安全策略和权限配置管理页面', true, NOW(), NOW()),
|
||||
(gen_random_uuid(), '日志配置', 'logging-settings', '日志级别和文件管理配置页面', true, NOW(), NOW()),
|
||||
(gen_random_uuid(), '缓存配置', 'cache-settings', '缓存策略和性能配置管理页面', true, NOW(), NOW());
|
||||
|
||||
-- 获取页面ID用于创建块
|
||||
DO $$
|
||||
DECLARE
|
||||
app_page_id UUID;
|
||||
database_page_id UUID;
|
||||
kafka_page_id UUID;
|
||||
security_page_id UUID;
|
||||
logging_page_id UUID;
|
||||
cache_page_id UUID;
|
||||
BEGIN
|
||||
-- 获取页面ID
|
||||
SELECT id INTO app_page_id FROM pages WHERE slug = 'app-settings';
|
||||
SELECT id INTO database_page_id FROM pages WHERE slug = 'database-settings';
|
||||
SELECT id INTO kafka_page_id FROM pages WHERE slug = 'kafka-settings';
|
||||
SELECT id INTO security_page_id FROM pages WHERE slug = 'security-settings';
|
||||
SELECT id INTO logging_page_id FROM pages WHERE slug = 'logging-settings';
|
||||
SELECT id INTO cache_page_id FROM pages WHERE slug = 'cache-settings';
|
||||
|
||||
-- 为应用配置页面创建块
|
||||
-- 1. 英雄块 - 页面标题
|
||||
INSERT INTO hero_blocks (id, page_id, block_order, title, subtitle, background_color, text_color, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), app_page_id, 1, '应用配置管理', '管理系统的基本配置信息', '#4F46E5', '#FFFFFF', true, NOW(), NOW());
|
||||
|
||||
-- 2. 设置块 - 应用配置
|
||||
INSERT INTO settings_blocks (id, page_id, block_order, title, category, editable, display_mode, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), app_page_id, 2, '应用基本配置', 'app', true, 'form', true, NOW(), NOW());
|
||||
|
||||
-- 3. 文本块 - 说明文档
|
||||
INSERT INTO text_blocks (id, page_id, block_order, title, markdown, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), app_page_id, 3, '配置说明', '## 应用配置说明
|
||||
|
||||
这里可以管理应用的基本配置信息,包括:
|
||||
|
||||
- **应用名称**: 系统显示的名称
|
||||
- **应用版本**: 当前系统版本号
|
||||
- **调试模式**: 是否启用调试功能
|
||||
- **系统时区**: 系统使用的时区设置
|
||||
|
||||
> 注意:这些配置项为系统级配置,修改后可能需要重启应用才能生效。', true, NOW(), NOW());
|
||||
|
||||
-- 为数据库配置页面创建块
|
||||
-- 1. 英雄块
|
||||
INSERT INTO hero_blocks (id, page_id, block_order, title, subtitle, background_color, text_color, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), database_page_id, 1, '数据库配置管理', '管理数据库连接和性能配置', '#059669', '#FFFFFF', true, NOW(), NOW());
|
||||
|
||||
-- 2. 设置块
|
||||
INSERT INTO settings_blocks (id, page_id, block_order, title, category, editable, display_mode, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), database_page_id, 2, '数据库配置', 'database', true, 'form', true, NOW(), NOW());
|
||||
|
||||
-- 3. 文本块
|
||||
INSERT INTO text_blocks (id, page_id, block_order, title, markdown, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), database_page_id, 3, '配置说明', '## 数据库配置说明
|
||||
|
||||
管理数据库相关的配置参数:
|
||||
|
||||
- **最大连接数**: 数据库连接池的最大连接数量
|
||||
- **连接超时**: 数据库连接的超时时间设置
|
||||
|
||||
> 警告:修改这些配置可能会影响系统性能,请谨慎操作。', true, NOW(), NOW());
|
||||
|
||||
-- 为Kafka配置页面创建块
|
||||
-- 1. 英雄块
|
||||
INSERT INTO hero_blocks (id, page_id, block_order, title, subtitle, background_color, text_color, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), kafka_page_id, 1, 'Kafka配置管理', '管理Kafka消息队列配置', '#DC2626', '#FFFFFF', true, NOW(), NOW());
|
||||
|
||||
-- 2. 设置块
|
||||
INSERT INTO settings_blocks (id, page_id, block_order, title, category, editable, display_mode, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), kafka_page_id, 2, 'Kafka配置', 'kafka', true, 'form', true, NOW(), NOW());
|
||||
|
||||
-- 3. 文本块
|
||||
INSERT INTO text_blocks (id, page_id, block_order, title, markdown, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), kafka_page_id, 3, '配置说明', '## Kafka配置说明
|
||||
|
||||
管理Kafka消息队列的相关配置:
|
||||
|
||||
- **最大重试次数**: 消息发送失败时的最大重试次数
|
||||
- **重试延迟**: 重试之间的延迟时间
|
||||
|
||||
> 提示:合理的重试配置可以提高消息传递的可靠性。', true, NOW(), NOW());
|
||||
|
||||
-- 为安全配置页面创建块
|
||||
-- 1. 英雄块
|
||||
INSERT INTO hero_blocks (id, page_id, block_order, title, subtitle, background_color, text_color, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), security_page_id, 1, '安全配置管理', '管理安全策略和权限配置', '#7C3AED', '#FFFFFF', true, NOW(), NOW());
|
||||
|
||||
-- 2. 设置块
|
||||
INSERT INTO settings_blocks (id, page_id, block_order, title, category, editable, display_mode, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), security_page_id, 2, '安全配置', 'security', true, 'form', true, NOW(), NOW());
|
||||
|
||||
-- 3. 文本块
|
||||
INSERT INTO text_blocks (id, page_id, block_order, title, markdown, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), security_page_id, 3, '配置说明', '## 安全配置说明
|
||||
|
||||
管理系统的安全相关配置:
|
||||
|
||||
- **会话超时**: 用户会话的超时时间
|
||||
- **最大登录尝试**: 允许的最大登录失败次数
|
||||
|
||||
> 重要:这些配置直接影响系统安全性,请根据实际需求谨慎设置。', true, NOW(), NOW());
|
||||
|
||||
-- 为日志配置页面创建块
|
||||
-- 1. 英雄块
|
||||
INSERT INTO hero_blocks (id, page_id, block_order, title, subtitle, background_color, text_color, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), logging_page_id, 1, '日志配置管理', '管理日志级别和文件管理配置', '#F59E0B', '#FFFFFF', true, NOW(), NOW());
|
||||
|
||||
-- 2. 设置块
|
||||
INSERT INTO settings_blocks (id, page_id, block_order, title, category, editable, display_mode, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), logging_page_id, 2, '日志配置', 'logging', true, 'form', true, NOW(), NOW());
|
||||
|
||||
-- 3. 文本块
|
||||
INSERT INTO text_blocks (id, page_id, block_order, title, markdown, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), logging_page_id, 3, '配置说明', '## 日志配置说明
|
||||
|
||||
管理系统日志的相关配置:
|
||||
|
||||
- **日志级别**: 记录日志的详细程度
|
||||
- **最大日志文件数**: 保留的日志文件数量
|
||||
|
||||
> 建议:生产环境建议使用 info 或 warn 级别,避免过多的日志输出。', true, NOW(), NOW());
|
||||
|
||||
-- 为缓存配置页面创建块
|
||||
-- 1. 英雄块
|
||||
INSERT INTO hero_blocks (id, page_id, block_order, title, subtitle, background_color, text_color, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), cache_page_id, 1, '缓存配置管理', '管理缓存策略和性能配置', '#10B981', '#FFFFFF', true, NOW(), NOW());
|
||||
|
||||
-- 2. 设置块
|
||||
INSERT INTO settings_blocks (id, page_id, block_order, title, category, editable, display_mode, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), cache_page_id, 2, '缓存配置', 'cache', true, 'form', true, NOW(), NOW());
|
||||
|
||||
-- 3. 文本块
|
||||
INSERT INTO text_blocks (id, page_id, block_order, title, markdown, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), cache_page_id, 3, '配置说明', '## 缓存配置说明
|
||||
|
||||
管理系统缓存的配置参数:
|
||||
|
||||
- **缓存生存时间**: 缓存数据的有效期
|
||||
- **缓存最大条目数**: 缓存中允许的最大条目数量
|
||||
|
||||
> 优化:合理的缓存配置可以显著提升系统性能。', true, NOW(), NOW());
|
||||
|
||||
END $$;
|
||||
|
||||
-- 创建索引以提高查询性能
|
||||
CREATE INDEX IF NOT EXISTS idx_pages_slug ON pages(slug);
|
||||
CREATE INDEX IF NOT EXISTS idx_hero_blocks_page_id ON hero_blocks(page_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_settings_blocks_page_id ON settings_blocks(page_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_text_blocks_page_id ON text_blocks(page_id);
|
||||
32
migrations/009_fix_timestamp_types.sql
Normal file
32
migrations/009_fix_timestamp_types.sql
Normal file
@ -0,0 +1,32 @@
|
||||
-- 修复时间戳类型不匹配问题
|
||||
-- 将 settings 表中的 TIMESTAMP 字段改为 TIMESTAMPTZ
|
||||
|
||||
-- 修改 settings 表的时间字段
|
||||
ALTER TABLE settings
|
||||
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC',
|
||||
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'UTC';
|
||||
|
||||
-- 修改 settings_history 表的时间字段
|
||||
ALTER TABLE settings_history
|
||||
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
||||
|
||||
-- 更新触发器函数以使用 TIMESTAMPTZ
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP AT TIME ZONE 'UTC';
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- 重新创建触发器
|
||||
DROP TRIGGER IF EXISTS update_settings_updated_at ON settings;
|
||||
CREATE TRIGGER update_settings_updated_at
|
||||
BEFORE UPDATE ON settings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 验证修改
|
||||
COMMENT ON COLUMN settings.created_at IS '创建时间 (UTC)';
|
||||
COMMENT ON COLUMN settings.updated_at IS '更新时间 (UTC)';
|
||||
COMMENT ON COLUMN settings_history.created_at IS '变更记录时间 (UTC)';
|
||||
128
migrations/010_casbin_rbac.sql
Normal file
128
migrations/010_casbin_rbac.sql
Normal file
@ -0,0 +1,128 @@
|
||||
-- Casbin RBAC 权限管理表
|
||||
-- 这些表用于存储 casbin 的 RBAC 模型和策略
|
||||
|
||||
-- 创建 casbin_rule 表,用于存储权限策略
|
||||
CREATE TABLE IF NOT EXISTS casbin_rule (
|
||||
id SERIAL PRIMARY KEY,
|
||||
ptype VARCHAR(10) NOT NULL, -- 策略类型:p(策略), g(角色组), e(效果), m(模型)
|
||||
v0 VARCHAR(256), -- 角色或用户
|
||||
v1 VARCHAR(256), -- 资源
|
||||
v2 VARCHAR(256), -- 操作
|
||||
v3 VARCHAR(256), -- 额外参数
|
||||
v4 VARCHAR(256), -- 额外参数
|
||||
v5 VARCHAR(256) -- 额外参数
|
||||
);
|
||||
|
||||
-- 创建索引以提高查询性能
|
||||
CREATE INDEX IF NOT EXISTS idx_casbin_rule_ptype ON casbin_rule(ptype);
|
||||
CREATE INDEX IF NOT EXISTS idx_casbin_rule_v0 ON casbin_rule(v0);
|
||||
CREATE INDEX IF NOT EXISTS idx_casbin_rule_v1 ON casbin_rule(v1);
|
||||
CREATE INDEX IF NOT EXISTS idx_casbin_rule_v2 ON casbin_rule(v2);
|
||||
|
||||
-- 插入默认的 RBAC 模型配置
|
||||
-- 这里使用标准的 RBAC 模型:用户 -> 角色 -> 权限
|
||||
INSERT INTO casbin_rule (ptype, v0, v1, v2, v3, v4, v5) VALUES
|
||||
-- 策略规则:角色可以访问的资源
|
||||
('p', 'admin', 'settings', 'read', '', '', ''),
|
||||
('p', 'admin', 'settings', 'write', '', '', ''),
|
||||
('p', 'admin', 'settings', 'delete', '', '', ''),
|
||||
('p', 'admin', 'users', 'read', '', '', ''),
|
||||
('p', 'admin', 'users', 'write', '', '', ''),
|
||||
('p', 'admin', 'users', 'delete', '', '', ''),
|
||||
('p', 'admin', 'invite_codes', 'read', '', '', ''),
|
||||
('p', 'admin', 'invite_codes', 'write', '', '', ''),
|
||||
('p', 'admin', 'invite_codes', 'delete', '', '', ''),
|
||||
('p', 'admin', 'pages', 'read', '', '', ''),
|
||||
('p', 'admin', 'pages', 'write', '', '', ''),
|
||||
('p', 'admin', 'pages', 'delete', '', '', ''),
|
||||
('p', 'admin', 'page_blocks', 'read', '', '', ''),
|
||||
('p', 'admin', 'page_blocks', 'write', '', '', ''),
|
||||
('p', 'admin', 'page_blocks', 'delete', '', '', ''),
|
||||
|
||||
('p', 'user', 'settings', 'read', '', '', ''),
|
||||
('p', 'user', 'pages', 'read', '', '', ''),
|
||||
('p', 'user', 'page_blocks', 'read', '', '', ''),
|
||||
('p', 'user', 'invite_codes', 'read', '', '', ''),
|
||||
('p', 'user', 'invite_codes', 'write', '', '', ''),
|
||||
|
||||
-- 角色组规则:用户属于哪个角色
|
||||
('g', 'admin@example.com', 'admin', '', '', '', ''),
|
||||
('g', 'user@example.com', 'user', '', '', '', '');
|
||||
|
||||
-- 创建权限资源表,用于管理可配置的权限
|
||||
CREATE TABLE IF NOT EXISTS permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
resource VARCHAR(255) NOT NULL,
|
||||
action VARCHAR(255) NOT NULL,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 创建角色权限关联表
|
||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
role_name VARCHAR(255) NOT NULL,
|
||||
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
|
||||
granted_by UUID REFERENCES users(id),
|
||||
granted_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(role_name, permission_id)
|
||||
);
|
||||
|
||||
-- 创建用户角色关联表(支持多角色)
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role_name VARCHAR(255) NOT NULL,
|
||||
granted_by UUID REFERENCES users(id),
|
||||
granted_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ,
|
||||
UNIQUE(user_id, role_name)
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_permissions_resource ON permissions(resource);
|
||||
CREATE INDEX IF NOT EXISTS idx_permissions_action ON permissions(action);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_permissions_role ON role_permissions(role_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_role ON user_roles(role_name);
|
||||
|
||||
-- 插入默认权限
|
||||
INSERT INTO permissions (name, description, resource, action, is_active, created_at, updated_at) VALUES
|
||||
-- 设置管理权限
|
||||
('settings_read', '读取系统设置', 'settings', 'read', true, NOW(), NOW()),
|
||||
('settings_write', '修改系统设置', 'settings', 'write', true, NOW(), NOW()),
|
||||
('settings_delete', '删除系统设置', 'settings', 'delete', true, NOW(), NOW()),
|
||||
|
||||
-- 用户管理权限
|
||||
('users_read', '读取用户信息', 'users', 'read', true, NOW(), NOW()),
|
||||
('users_write', '修改用户信息', 'users', 'write', true, NOW(), NOW()),
|
||||
('users_delete', '删除用户', 'users', 'delete', true, NOW(), NOW()),
|
||||
|
||||
-- 邀请码管理权限
|
||||
('invite_codes_read', '读取邀请码', 'invite_codes', 'read', true, NOW(), NOW()),
|
||||
('invite_codes_write', '创建邀请码', 'invite_codes', 'write', true, NOW(), NOW()),
|
||||
('invite_codes_delete', '删除邀请码', 'invite_codes', 'delete', true, NOW(), NOW()),
|
||||
|
||||
-- 页面管理权限
|
||||
('pages_read', '读取页面', 'pages', 'read', true, NOW(), NOW()),
|
||||
('pages_write', '创建/修改页面', 'pages', 'write', true, NOW(), NOW()),
|
||||
('pages_delete', '删除页面', 'pages', 'delete', true, NOW(), NOW()),
|
||||
|
||||
-- 页面块管理权限
|
||||
('page_blocks_read', '读取页面块', 'page_blocks', 'read', true, NOW(), NOW()),
|
||||
('page_blocks_write', '创建/修改页面块', 'page_blocks', 'write', true, NOW(), NOW()),
|
||||
('page_blocks_delete', '删除页面块', 'page_blocks', 'delete', true, NOW(), NOW());
|
||||
|
||||
-- 为角色分配权限
|
||||
INSERT INTO role_permissions (role_name, permission_id, granted_by, granted_at)
|
||||
SELECT 'admin', id, NULL, NOW() FROM permissions;
|
||||
|
||||
INSERT INTO role_permissions (role_name, permission_id, granted_by, granted_at)
|
||||
SELECT 'user', id, NULL, NOW() FROM permissions WHERE action = 'read' OR (resource = 'invite_codes' AND action = 'write');
|
||||
|
||||
-- 创建触发器更新 updated_at
|
||||
CREATE TRIGGER update_permissions_updated_at BEFORE UPDATE
|
||||
ON permissions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
43
migrations/011_create_page_blocks_table.sql
Normal file
43
migrations/011_create_page_blocks_table.sql
Normal file
@ -0,0 +1,43 @@
|
||||
-- 创建通用的页面块表
|
||||
-- 这个表用于存储页面的各种内容块,支持多种块类型
|
||||
|
||||
CREATE TABLE IF NOT EXISTS page_blocks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
page_id UUID NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
block_order INTEGER NOT NULL,
|
||||
title VARCHAR(255),
|
||||
block_type VARCHAR(50) NOT NULL, -- 'text', 'chart', 'settings', 'table', 'hero', etc.
|
||||
content JSONB, -- 块内容,根据类型存储不同的配置
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
created_by UUID REFERENCES users(id),
|
||||
updated_by UUID REFERENCES users(id),
|
||||
UNIQUE(page_id, block_order)
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_page_blocks_page_id ON page_blocks(page_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_page_blocks_block_type ON page_blocks(block_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_page_blocks_is_active ON page_blocks(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_page_blocks_order ON page_blocks(page_id, block_order);
|
||||
|
||||
-- 创建更新时间触发器
|
||||
CREATE TRIGGER update_page_blocks_updated_at BEFORE UPDATE ON page_blocks
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 插入一些示例页面块数据
|
||||
INSERT INTO page_blocks (id, page_id, block_order, title, block_type, content, is_active, created_at, updated_at) VALUES
|
||||
-- 系统设置页面的块
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'system-settings'), 1, '系统配置', 'settings', '{"category": "system", "editable": true, "display_mode": "form"}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'system-settings'), 2, '安全设置', 'settings', '{"category": "security", "editable": true, "display_mode": "form"}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
|
||||
-- 数据概览页面的块
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'data-overview'), 1, '统计概览', 'chart', '{"chart_type": "dashboard", "config": {"layout": "grid"}}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'data-overview'), 2, '趋势图表', 'chart', '{"chart_type": "line", "config": {"x_axis": "time", "y_axis": "value"}}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
|
||||
-- 用户管理页面的块
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'user-management'), 1, '用户列表', 'table', '{"data_source": "users", "data_config": {"columns": ["id", "username", "email", "status"]}}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'user-management'), 2, '用户统计', 'chart', '{"chart_type": "pie", "config": {"title": "用户状态分布"}}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
|
||||
ON CONFLICT DO NOTHING;
|
||||
123
migrations/012_site_ops_settings.sql
Normal file
123
migrations/012_site_ops_settings.sql
Normal file
@ -0,0 +1,123 @@
|
||||
-- 插入站点与运营配置
|
||||
-- 一、站点信息
|
||||
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
|
||||
-- 站点基本信息
|
||||
('site.name', 'MMAP System', 'string', '站点名称', 'site', true, true),
|
||||
('site.locale_default', 'zh-CN', 'string', '默认语言', 'site', true, true),
|
||||
('site.locales_supported', '["zh-CN", "en"]', 'json', '支持的语言列表', 'site', true, true),
|
||||
|
||||
-- 品牌配置
|
||||
('site.brand.logo_url', '/images/logo.png', 'string', 'Logo URL', 'site', true, true),
|
||||
('site.brand.primary_color', '#3B82F6', 'string', '主题色', 'site', true, true),
|
||||
('site.brand.dark_mode_default', 'false', 'boolean', '暗黑模式默认开启', 'site', true, true),
|
||||
|
||||
-- 页脚链接
|
||||
('site.footer_links', '[
|
||||
{"name": "关于我们", "url": "/about", "visible_to_guest": true},
|
||||
{"name": "联系我们", "url": "/contact", "visible_to_guest": true},
|
||||
{"name": "用户中心", "url": "/dashboard", "visible_to_guest": false}
|
||||
]', 'json', '页脚链接配置', 'site', true, true)
|
||||
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- 二、公告/维护配置
|
||||
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
|
||||
-- 横幅公告
|
||||
('notice.banner.enabled', 'false', 'boolean', '横幅公告开关', 'notice', true, true),
|
||||
('notice.banner.text', '{"zh-CN": "欢迎使用MMAP系统", "en": "Welcome to MMAP System"}', 'json', '横幅公告多语言文本', 'notice', true, true),
|
||||
|
||||
-- 维护窗口
|
||||
('maintenance.window', '{
|
||||
"enabled": false,
|
||||
"start_time": "2024-01-01T02:00:00Z",
|
||||
"end_time": "2024-01-01T06:00:00Z",
|
||||
"message": {"zh-CN": "系统维护中,请稍后再试", "en": "System maintenance in progress"}
|
||||
}', 'json', '维护窗口配置', 'maintenance', true, true),
|
||||
|
||||
-- 弹窗公告
|
||||
('modal.announcements', '[
|
||||
{
|
||||
"id": "welcome_2024",
|
||||
"title": {"zh-CN": "2024新年快乐", "en": "Happy New Year 2024"},
|
||||
"content": {"zh-CN": "感谢您在过去一年的支持", "en": "Thank you for your support in the past year"},
|
||||
"start_time": "2024-01-01T00:00:00Z",
|
||||
"end_time": "2024-01-31T23:59:59Z",
|
||||
"audience": ["all"],
|
||||
"priority": "high"
|
||||
}
|
||||
]', 'json', '弹窗公告列表', 'notice', true, true)
|
||||
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- 三、文档/帮助配置
|
||||
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
|
||||
-- 文档链接
|
||||
('docs.links', '[
|
||||
{"name": "API文档", "url": "/docs/api", "description": "完整的API接口文档"},
|
||||
{"name": "图例说明", "url": "/docs/legend", "description": "系统图例和符号说明"},
|
||||
{"name": "计费说明", "url": "/docs/billing", "description": "详细的计费规则和说明"},
|
||||
{"name": "用户手册", "url": "/docs/user-guide", "description": "用户操作指南"}
|
||||
]', 'json', '文档链接配置', 'docs', true, true),
|
||||
|
||||
-- 支持渠道
|
||||
('support.channels', '{
|
||||
"email": "support@mapp.com",
|
||||
"ticket_system": "/support/tickets",
|
||||
"chat_groups": [
|
||||
{"name": "官方QQ群", "url": "https://qm.qq.com/xxx", "description": "技术交流群"},
|
||||
{"name": "微信群", "qr_code": "/images/wechat-qr.png", "description": "扫码加入微信群"}
|
||||
],
|
||||
"working_hours": {"zh-CN": "周一至周五 9:00-18:00", "en": "Mon-Fri 9:00-18:00"}
|
||||
}', 'json', '支持渠道配置', 'support', true, true)
|
||||
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- 四、运营配置
|
||||
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
|
||||
-- 功能开关
|
||||
('ops.features.registration_enabled', 'true', 'boolean', '用户注册功能开关', 'ops', true, true),
|
||||
('ops.features.invite_code_required', 'true', 'boolean', '注册是否需要邀请码', 'ops', true, true),
|
||||
('ops.features.email_verification', 'false', 'boolean', '邮箱验证功能开关', 'ops', true, true),
|
||||
|
||||
-- 限制配置
|
||||
('ops.limits.max_users', '1000', 'number', '最大用户数限制', 'ops', true, true),
|
||||
('ops.limits.max_invite_codes_per_user', '10', 'number', '每个用户最大邀请码数量', 'ops', true, true),
|
||||
('ops.limits.session_timeout_hours', '24', 'number', '会话超时时间(小时)', 'ops', true, true),
|
||||
|
||||
-- 通知配置
|
||||
('ops.notifications.welcome_email', 'true', 'boolean', '发送欢迎邮件', 'ops', true, true),
|
||||
('ops.notifications.system_announcements', 'true', 'boolean', '系统公告通知', 'ops', true, true),
|
||||
('ops.notifications.maintenance_alerts', 'true', 'boolean', '维护提醒通知', 'ops', true, true)
|
||||
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- 创建配置分类页面
|
||||
INSERT INTO pages (id, title, slug, description, is_active, created_at, updated_at) VALUES
|
||||
(gen_random_uuid(), '站点配置', 'site-settings', '站点基本信息、品牌、页脚等配置管理', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), '公告维护', 'notice-maintenance', '横幅公告、维护窗口、弹窗公告等配置管理', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), '文档支持', 'docs-support', '文档链接、支持渠道等配置管理', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), '运营配置', 'ops-settings', '功能开关、限制配置、通知配置等管理', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- 为每个页面创建配置块
|
||||
INSERT INTO page_blocks (id, page_id, block_order, title, block_type, content, is_active, created_at, updated_at) VALUES
|
||||
-- 站点配置页面
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'site-settings'), 1, '站点基本信息', 'settings', '{"category": "site", "editable": true, "display_mode": "form"}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'site-settings'), 2, '品牌配置', 'settings', '{"category": "site", "editable": true, "display_mode": "form"}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
|
||||
-- 公告维护页面
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'notice-maintenance'), 1, '横幅公告', 'settings', '{"category": "notice", "editable": true, "display_mode": "form"}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'notice-maintenance'), 2, '维护窗口', 'settings', '{"category": "maintenance", "editable": true, "display_mode": "form"}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'notice-maintenance'), 3, '弹窗公告', 'settings', '{"category": "notice", "editable": true, "display_mode": "table"}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
|
||||
-- 文档支持页面
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'docs-support'), 1, '文档链接', 'settings', '{"category": "docs", "editable": true, "display_mode": "table"}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'docs-support'), 2, '支持渠道', 'settings', '{"category": "support", "editable": true, "display_mode": "form"}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
|
||||
-- 运营配置页面
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'ops-settings'), 1, '功能开关', 'settings', '{"category": "ops", "editable": true, "display_mode": "form"}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'ops-settings'), 2, '限制配置', 'settings', '{"category": "ops", "editable": true, "display_mode": "form"}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(gen_random_uuid(), (SELECT id FROM pages WHERE slug = 'ops-settings'), 3, '通知配置', 'settings', '{"category": "ops", "editable": true, "display_mode": "form"}', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
|
||||
ON CONFLICT DO NOTHING;
|
||||
17
src/app.rs
17
src/app.rs
@ -24,9 +24,10 @@ use crate::{
|
||||
config::Config,
|
||||
graphql::{subscription::StatusUpdate, MutationRoot, QueryRoot, SubscriptionRoot},
|
||||
services::{
|
||||
invite_code_service::InviteCodeService, mosaic_service::MosaicService,
|
||||
system_config_service::SystemConfigService, user_service::UserService,
|
||||
settings_service::SettingsService,
|
||||
casbin_service::CasbinService, invite_code_service::InviteCodeService,
|
||||
mosaic_service::MosaicService, page_block_service::PageBlockService,
|
||||
settings_service::SettingsService, system_config_service::SystemConfigService,
|
||||
user_service::UserService,
|
||||
},
|
||||
};
|
||||
|
||||
@ -42,7 +43,7 @@ pub struct AppState {
|
||||
pub status_sender: Option<broadcast::Sender<StatusUpdate>>,
|
||||
}
|
||||
|
||||
pub fn create_router(
|
||||
pub async fn create_router(
|
||||
pool: PgPool,
|
||||
config: Config,
|
||||
status_sender: Option<broadcast::Sender<StatusUpdate>>,
|
||||
@ -52,6 +53,11 @@ pub fn create_router(
|
||||
let system_config_service = SystemConfigService::new(pool.clone());
|
||||
let mosaic_service = MosaicService::new(pool.clone());
|
||||
let settings_service = SettingsService::new(pool.clone());
|
||||
let page_block_service = PageBlockService::new(pool.clone());
|
||||
|
||||
let casbin_service = CasbinService::new(config.database_url.clone())
|
||||
.await
|
||||
.expect("Failed to initialize CasbinService");
|
||||
|
||||
let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
|
||||
.data(pool)
|
||||
@ -60,6 +66,8 @@ pub fn create_router(
|
||||
.data(system_config_service)
|
||||
.data(mosaic_service)
|
||||
.data(settings_service)
|
||||
.data(page_block_service)
|
||||
.data(casbin_service)
|
||||
.data(config.clone())
|
||||
.data(status_sender.clone())
|
||||
.finish();
|
||||
@ -94,7 +102,6 @@ pub fn create_router(
|
||||
// (Method::POST, "/graphql"),
|
||||
// Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()),
|
||||
// )
|
||||
// .with_global_fallback(false)
|
||||
// .with_gc_interval(1000)
|
||||
// .default_handle_error(),
|
||||
// )
|
||||
|
||||
120
src/cli.rs
120
src/cli.rs
@ -16,6 +16,8 @@ pub enum Commands {
|
||||
Serve(ServeArgs),
|
||||
/// 运行数据库迁移
|
||||
Migrate(MigrateArgs),
|
||||
/// 权限管理
|
||||
Permissions(PermissionsArgs),
|
||||
/// 显示版本信息
|
||||
Version,
|
||||
/// 显示配置信息
|
||||
@ -60,6 +62,124 @@ pub struct MigrateArgs {
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
/// 权限管理相关命令
|
||||
#[derive(Subcommand)]
|
||||
pub enum PermissionsCommands {
|
||||
/// 列出所有权限策略
|
||||
List,
|
||||
/// 添加权限策略
|
||||
Add(AddPolicyArgs),
|
||||
/// 移除权限策略
|
||||
Remove(RemovePolicyArgs),
|
||||
/// 为用户分配角色
|
||||
AssignRole(AssignRoleArgs),
|
||||
/// 移除用户角色
|
||||
RemoveRole(RemoveRoleArgs),
|
||||
/// 列出用户角色
|
||||
ListUserRoles(ListUserRolesArgs),
|
||||
/// 列出角色权限
|
||||
ListRolePermissions(ListRolePermissionsArgs),
|
||||
/// 重新加载权限策略
|
||||
Reload,
|
||||
/// 检查用户权限
|
||||
Check(CheckPermissionArgs),
|
||||
}
|
||||
|
||||
/// 权限管理参数
|
||||
#[derive(Args)]
|
||||
pub struct PermissionsArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: PermissionsCommands,
|
||||
}
|
||||
|
||||
/// 添加权限策略参数
|
||||
#[derive(Args)]
|
||||
pub struct AddPolicyArgs {
|
||||
/// 角色名称
|
||||
#[arg(short, long)]
|
||||
pub role: String,
|
||||
|
||||
/// 资源名称
|
||||
#[arg(short, long)]
|
||||
pub resource: String,
|
||||
|
||||
/// 操作名称
|
||||
#[arg(short, long)]
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
/// 移除权限策略参数
|
||||
#[derive(Args)]
|
||||
pub struct RemovePolicyArgs {
|
||||
/// 角色名称
|
||||
#[arg(short, long)]
|
||||
pub role: String,
|
||||
|
||||
/// 资源名称
|
||||
#[arg(short, long)]
|
||||
pub resource: String,
|
||||
|
||||
/// 操作名称
|
||||
#[arg(short, long)]
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
/// 分配角色参数
|
||||
#[derive(Args)]
|
||||
pub struct AssignRoleArgs {
|
||||
/// 用户ID
|
||||
#[arg(short, long)]
|
||||
pub user_id: String,
|
||||
|
||||
/// 角色名称
|
||||
#[arg(short, long)]
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
/// 移除角色参数
|
||||
#[derive(Args)]
|
||||
pub struct RemoveRoleArgs {
|
||||
/// 用户ID
|
||||
#[arg(short, long)]
|
||||
pub user_id: String,
|
||||
|
||||
/// 角色名称
|
||||
#[arg(short, long)]
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
/// 列出用户角色参数
|
||||
#[derive(Args)]
|
||||
pub struct ListUserRolesArgs {
|
||||
/// 用户ID
|
||||
#[arg(short, long)]
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
/// 列出角色权限参数
|
||||
#[derive(Args)]
|
||||
pub struct ListRolePermissionsArgs {
|
||||
/// 角色名称
|
||||
#[arg(short, long)]
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
/// 检查权限参数
|
||||
#[derive(Args)]
|
||||
pub struct CheckPermissionArgs {
|
||||
/// 用户ID
|
||||
#[arg(short, long)]
|
||||
pub user_id: String,
|
||||
|
||||
/// 资源名称
|
||||
#[arg(short, long)]
|
||||
pub resource: String,
|
||||
|
||||
/// 操作名称
|
||||
#[arg(short, long)]
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
impl Default for Commands {
|
||||
fn default() -> Self {
|
||||
Commands::Serve(ServeArgs {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use crate::auth::get_auth_user;
|
||||
use crate::models::user::Role;
|
||||
use crate::{auth::get_auth_user, services::casbin_service::CasbinService};
|
||||
use async_graphql::{Context, Error, Guard, Result};
|
||||
use tracing::warn;
|
||||
|
||||
pub struct RequireRole(pub Role);
|
||||
|
||||
@ -18,3 +19,201 @@ impl Guard for RequireRole {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 基于 Casbin 的权限检查 Guard
|
||||
pub struct RequirePermission {
|
||||
resource: String,
|
||||
action: String,
|
||||
}
|
||||
|
||||
impl RequirePermission {
|
||||
pub fn new(resource: &str, action: &str) -> Self {
|
||||
Self {
|
||||
resource: resource.to_string(),
|
||||
action: action.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Guard for RequirePermission {
|
||||
async fn check(&self, ctx: &Context<'_>) -> Result<()> {
|
||||
let user = get_auth_user(ctx).await?;
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
// 检查用户是否有权限
|
||||
let has_permission = casbin_service
|
||||
.check_permission(&user.id.to_string(), &self.resource, &self.action)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to check permission: {}", e);
|
||||
Error::new("Permission check failed")
|
||||
})?;
|
||||
|
||||
if has_permission {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(format!(
|
||||
"Insufficient permissions. Required: {} {} on {}",
|
||||
self.action, self.resource, self.resource
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查读取权限的 Guard
|
||||
pub struct RequireReadPermission {
|
||||
resource: String,
|
||||
}
|
||||
|
||||
impl RequireReadPermission {
|
||||
pub fn new(resource: &str) -> Self {
|
||||
Self {
|
||||
resource: resource.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Guard for RequireReadPermission {
|
||||
async fn check(&self, ctx: &Context<'_>) -> Result<()> {
|
||||
let user = get_auth_user(ctx).await?;
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let has_permission = casbin_service
|
||||
.can_read(&user.id.to_string(), &self.resource)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to check read permission: {}", e);
|
||||
Error::new("Permission check failed")
|
||||
})?;
|
||||
|
||||
if has_permission {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(format!(
|
||||
"Insufficient permissions. Required: read on {}",
|
||||
self.resource
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查写入权限的 Guard
|
||||
pub struct RequireWritePermission {
|
||||
resource: String,
|
||||
}
|
||||
|
||||
impl RequireWritePermission {
|
||||
pub fn new(resource: &str) -> Self {
|
||||
Self {
|
||||
resource: resource.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Guard for RequireWritePermission {
|
||||
async fn check(&self, ctx: &Context<'_>) -> Result<()> {
|
||||
let user = get_auth_user(ctx).await?;
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let has_permission = casbin_service
|
||||
.can_write(&user.id.to_string(), &self.resource)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to check write permission: {}", e);
|
||||
Error::new("Permission check failed")
|
||||
})?;
|
||||
|
||||
if has_permission {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(format!(
|
||||
"Insufficient permissions. Required: write on {}",
|
||||
self.resource
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查删除权限的 Guard
|
||||
pub struct RequireDeletePermission {
|
||||
resource: String,
|
||||
}
|
||||
|
||||
impl RequireDeletePermission {
|
||||
pub fn new(resource: &str) -> Self {
|
||||
Self {
|
||||
resource: resource.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Guard for RequireDeletePermission {
|
||||
async fn check(&self, ctx: &Context<'_>) -> Result<()> {
|
||||
let user = get_auth_user(ctx).await?;
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let has_permission = casbin_service
|
||||
.can_delete(&user.id.to_string(), &self.resource)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to check delete permission: {}", e);
|
||||
Error::new("Permission check failed")
|
||||
})?;
|
||||
|
||||
if has_permission {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(format!(
|
||||
"Insufficient permissions. Required: delete on {}",
|
||||
self.resource
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 组合权限检查 Guard
|
||||
pub struct RequireMultiplePermissions {
|
||||
permissions: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl RequireMultiplePermissions {
|
||||
pub fn new(permissions: &[(&str, &str)]) -> Self {
|
||||
Self {
|
||||
permissions: permissions
|
||||
.iter()
|
||||
.map(|(r, a)| (r.to_string(), a.to_string()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Guard for RequireMultiplePermissions {
|
||||
async fn check(&self, ctx: &Context<'_>) -> Result<()> {
|
||||
let user = get_auth_user(ctx).await?;
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let permissions: Vec<(&str, &str)> = self
|
||||
.permissions
|
||||
.iter()
|
||||
.map(|(r, a)| (r.as_str(), a.as_str()))
|
||||
.collect();
|
||||
|
||||
let results = casbin_service
|
||||
.check_permissions(&user.id.to_string(), &permissions)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to check multiple permissions: {}", e);
|
||||
Error::new("Permission check failed")
|
||||
})?;
|
||||
|
||||
// 所有权限都必须满足
|
||||
if results.iter().all(|&has_permission| has_permission) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(format!(
|
||||
"Insufficient permissions. Required: {:?}",
|
||||
self.permissions
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1053
src/graphql/query.rs
1053
src/graphql/query.rs
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,6 @@
|
||||
use crate::models::settings::{
|
||||
CreateSetting, Setting, SettingFilter, SettingHistory, UpdateSetting,
|
||||
};
|
||||
use crate::models::user::Role;
|
||||
use async_graphql::{InputObject, SimpleObject};
|
||||
use crate::models::{user::Role, Setting};
|
||||
use async_graphql::{InputObject, SimpleObject, Union};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -77,7 +75,7 @@ pub struct UserInfoRespnose {
|
||||
}
|
||||
|
||||
// Settings GraphQL types
|
||||
#[derive(SimpleObject)]
|
||||
#[derive(SimpleObject, Debug, Clone)]
|
||||
pub struct SettingType {
|
||||
pub id: Uuid,
|
||||
pub key: String,
|
||||
@ -94,6 +92,26 @@ pub struct SettingType {
|
||||
pub updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<Setting> for SettingType {
|
||||
fn from(setting: Setting) -> Self {
|
||||
SettingType {
|
||||
id: setting.id,
|
||||
key: setting.key,
|
||||
value: setting.value,
|
||||
value_type: setting.value_type,
|
||||
description: setting.description,
|
||||
category: setting.category,
|
||||
is_encrypted: setting.is_encrypted,
|
||||
is_system: setting.is_system,
|
||||
is_editable: setting.is_editable,
|
||||
created_at: setting.created_at,
|
||||
updated_at: setting.updated_at,
|
||||
created_by: setting.created_by,
|
||||
updated_by: setting.updated_by,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateSettingInput {
|
||||
pub key: String,
|
||||
@ -139,13 +157,545 @@ pub struct SettingsStatsType {
|
||||
pub stats: std::collections::HashMap<String, i64>,
|
||||
}
|
||||
|
||||
// Page Block GraphQL types
|
||||
#[derive(SimpleObject)]
|
||||
pub struct PageType {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
pub created_by: Option<Uuid>,
|
||||
pub updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Union)]
|
||||
pub enum BlockType {
|
||||
TextBlock(TextBlockType),
|
||||
ChartBlock(ChartBlockType),
|
||||
SettingsBlock(SettingsBlockType),
|
||||
TableBlock(TableBlockType),
|
||||
HeroBlock(HeroBlockType),
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct TextBlockType {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub markdown: String,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct ChartBlockType {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: String,
|
||||
pub chart_type: String,
|
||||
pub series: Vec<DataPointType>,
|
||||
pub config: Option<Value>,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct DataPointType {
|
||||
pub id: Uuid,
|
||||
pub chart_block_id: Uuid,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub label: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct SettingsBlockType {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub category: String,
|
||||
pub editable: bool,
|
||||
pub display_mode: String,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct TableBlockType {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub columns: Vec<TableColumnType>,
|
||||
pub data_source: String,
|
||||
pub data_config: Option<Value>,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct TableColumnType {
|
||||
pub id: Uuid,
|
||||
pub table_block_id: Uuid,
|
||||
pub name: String,
|
||||
pub label: String,
|
||||
pub data_type: String,
|
||||
pub is_sortable: bool,
|
||||
pub is_filterable: bool,
|
||||
pub width: Option<i32>,
|
||||
pub order: i32,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct HeroBlockType {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: String,
|
||||
pub subtitle: Option<String>,
|
||||
pub background_image: Option<String>,
|
||||
pub background_color: Option<String>,
|
||||
pub text_color: Option<String>,
|
||||
pub cta_text: Option<String>,
|
||||
pub cta_link: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
// Page Block Input types
|
||||
#[derive(InputObject)]
|
||||
pub struct BatchUpdateSettingsInput {
|
||||
pub updates: Vec<BatchUpdateItemInput>,
|
||||
pub struct CreatePageInputType {
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct BatchUpdateItemInput {
|
||||
pub key: String,
|
||||
pub value: Value,
|
||||
pub struct UpdatePageInputType {
|
||||
pub title: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateTextBlockInputType {
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub markdown: String,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateChartBlockInputType {
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: String,
|
||||
pub chart_type: String,
|
||||
pub series: Vec<CreateDataPointInputType>,
|
||||
pub config: Option<Value>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateDataPointInputType {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub label: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateSettingsBlockInputType {
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub category: String,
|
||||
pub editable: bool,
|
||||
pub display_mode: String,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct PageFilterInputType {
|
||||
pub title: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
// Enhanced Settings types for the settings center
|
||||
#[derive(SimpleObject)]
|
||||
pub struct SettingCenterType {
|
||||
pub id: Uuid,
|
||||
pub key: String,
|
||||
pub value: Option<String>,
|
||||
pub value_type: String,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_editable: Option<bool>,
|
||||
pub is_system: Option<bool>,
|
||||
pub description: Option<String>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct UpdateSettingCenterInput {
|
||||
pub key: String,
|
||||
pub value: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct BatchUpdateSettingsInput {
|
||||
pub updates: Vec<UpdateSettingCenterInput>,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// 配置分类页面类型,包含页面信息和相关配置项
|
||||
#[derive(SimpleObject)]
|
||||
pub struct CategoryPageType {
|
||||
/// 页面信息,如果不存在则为 None
|
||||
pub page: Option<PageType>,
|
||||
/// 该分类下的所有配置项
|
||||
pub settings: Vec<SettingCenterType>,
|
||||
/// 分类名称
|
||||
pub category: String,
|
||||
/// 该分类下的配置项总数
|
||||
pub settings_count: i32,
|
||||
/// 该分类下的系统配置项数量
|
||||
pub system_settings_count: i32,
|
||||
/// 该分类下的可编辑配置项数量
|
||||
pub editable_settings_count: i32,
|
||||
}
|
||||
|
||||
/// 权限类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct PermissionType {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 角色权限关联类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct RolePermissionType {
|
||||
pub id: Uuid,
|
||||
pub role_name: String,
|
||||
pub permission: PermissionType,
|
||||
pub granted_by: Option<Uuid>,
|
||||
pub granted_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 用户角色关联类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct UserRoleType {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub role_name: String,
|
||||
pub granted_by: Option<Uuid>,
|
||||
pub granted_at: chrono::DateTime<chrono::Utc>,
|
||||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
/// 权限策略类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct PolicyType {
|
||||
pub role: String,
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
/// 权限检查结果类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct PermissionCheckResult {
|
||||
pub user_id: String,
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
pub has_permission: bool,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
/// 权限管理查询输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct PermissionFilterInput {
|
||||
pub resource: Option<String>,
|
||||
pub action: Option<String>,
|
||||
pub role_name: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
/// 角色权限分配输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct AssignRolePermissionInput {
|
||||
pub role_name: String,
|
||||
pub permission_id: Uuid,
|
||||
pub granted_by: Uuid,
|
||||
}
|
||||
|
||||
/// 用户角色分配输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct AssignUserRoleInput {
|
||||
pub user_id: Uuid,
|
||||
pub role_name: String,
|
||||
pub granted_by: Uuid,
|
||||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct PermissionPair {
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
// 站点与运营配置相关类型
|
||||
|
||||
/// 站点基本信息类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct SiteInfoType {
|
||||
pub name: String,
|
||||
pub locale_default: String,
|
||||
pub locales_supported: Vec<String>,
|
||||
}
|
||||
|
||||
/// 品牌配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct BrandConfigType {
|
||||
pub logo_url: String,
|
||||
pub primary_color: String,
|
||||
pub dark_mode_default: bool,
|
||||
}
|
||||
|
||||
/// 页脚链接类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct FooterLinkType {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub visible_to_guest: bool,
|
||||
}
|
||||
|
||||
/// 站点配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct SiteConfigType {
|
||||
pub info: SiteInfoType,
|
||||
pub brand: BrandConfigType,
|
||||
pub footer_links: Vec<FooterLinkType>,
|
||||
}
|
||||
|
||||
/// 横幅公告类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct BannerNoticeType {
|
||||
pub enabled: bool,
|
||||
pub text: std::collections::HashMap<String, String>, // 多语言文本
|
||||
}
|
||||
|
||||
/// 维护窗口类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct MaintenanceWindowType {
|
||||
pub enabled: bool,
|
||||
pub start_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub end_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub message: std::collections::HashMap<String, String>, // 多语言消息
|
||||
}
|
||||
|
||||
/// 弹窗公告类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct ModalAnnouncementType {
|
||||
pub id: String,
|
||||
pub title: std::collections::HashMap<String, String>, // 多语言标题
|
||||
pub content: std::collections::HashMap<String, String>, // 多语言内容
|
||||
pub start_time: chrono::DateTime<chrono::Utc>,
|
||||
pub end_time: chrono::DateTime<chrono::Utc>,
|
||||
pub audience: Vec<String>,
|
||||
pub priority: String,
|
||||
}
|
||||
|
||||
/// 公告维护配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct NoticeMaintenanceType {
|
||||
pub banner: BannerNoticeType,
|
||||
pub maintenance_window: MaintenanceWindowType,
|
||||
pub modal_announcements: Vec<ModalAnnouncementType>,
|
||||
}
|
||||
|
||||
/// 文档链接类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct DocLinkType {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// 聊天群组类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct ChatGroupType {
|
||||
pub name: String,
|
||||
pub url: Option<String>,
|
||||
pub qr_code: Option<String>,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// 支持渠道类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct SupportChannelsType {
|
||||
pub email: String,
|
||||
pub ticket_system: String,
|
||||
pub chat_groups: Vec<ChatGroupType>,
|
||||
pub working_hours: std::collections::HashMap<String, String>, // 多语言工作时间
|
||||
}
|
||||
|
||||
/// 文档支持配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct DocsSupportType {
|
||||
pub links: Vec<DocLinkType>,
|
||||
pub channels: SupportChannelsType,
|
||||
}
|
||||
|
||||
/// 功能开关类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct FeatureSwitchesType {
|
||||
pub registration_enabled: bool,
|
||||
pub invite_code_required: bool,
|
||||
pub email_verification: bool,
|
||||
}
|
||||
|
||||
/// 限制配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct LimitsConfigType {
|
||||
pub max_users: i32,
|
||||
pub max_invite_codes_per_user: i32,
|
||||
pub session_timeout_hours: i32,
|
||||
}
|
||||
|
||||
/// 通知配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct NotificationConfigType {
|
||||
pub welcome_email: bool,
|
||||
pub system_announcements: bool,
|
||||
pub maintenance_alerts: bool,
|
||||
}
|
||||
|
||||
/// 运营配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct OpsConfigType {
|
||||
pub features: FeatureSwitchesType,
|
||||
pub limits: LimitsConfigType,
|
||||
pub notifications: NotificationConfigType,
|
||||
}
|
||||
|
||||
/// 完整的站点与运营配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct SiteOpsConfigType {
|
||||
pub site: SiteConfigType,
|
||||
pub notice_maintenance: NoticeMaintenanceType,
|
||||
pub docs_support: DocsSupportType,
|
||||
pub ops: OpsConfigType,
|
||||
}
|
||||
|
||||
/// 更新站点配置输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct UpdateSiteConfigInput {
|
||||
pub name: Option<String>,
|
||||
pub locale_default: Option<String>,
|
||||
pub locales_supported: Option<Vec<String>>,
|
||||
pub logo_url: Option<String>,
|
||||
pub primary_color: Option<String>,
|
||||
pub dark_mode_default: Option<bool>,
|
||||
pub footer_links: Option<Vec<FooterLinkInput>>,
|
||||
}
|
||||
|
||||
/// 页脚链接输入类型
|
||||
#[derive(Debug, InputObject, Serialize, Deserialize)]
|
||||
pub struct FooterLinkInput {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub visible_to_guest: bool,
|
||||
}
|
||||
|
||||
/// 更新公告配置输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct UpdateNoticeConfigInput {
|
||||
pub banner_enabled: Option<bool>,
|
||||
pub banner_text: Option<std::collections::HashMap<String, String>>,
|
||||
pub maintenance_enabled: Option<bool>,
|
||||
pub maintenance_start_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub maintenance_end_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub maintenance_message: Option<std::collections::HashMap<String, String>>,
|
||||
}
|
||||
|
||||
/// 更新弹窗公告输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct UpdateModalAnnouncementInput {
|
||||
pub id: String,
|
||||
pub title: Option<std::collections::HashMap<String, String>>,
|
||||
pub content: Option<std::collections::HashMap<String, String>>,
|
||||
pub start_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub end_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub audience: Option<Vec<String>>,
|
||||
pub priority: Option<String>,
|
||||
}
|
||||
|
||||
/// 更新文档支持配置输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct UpdateDocsSupportInput {
|
||||
pub links: Option<Vec<DocLinkInput>>,
|
||||
pub email: Option<String>,
|
||||
pub ticket_system: Option<String>,
|
||||
pub working_hours: Option<std::collections::HashMap<String, String>>,
|
||||
}
|
||||
|
||||
/// 文档链接输入类型
|
||||
#[derive(Debug, InputObject, Serialize, Deserialize)]
|
||||
pub struct DocLinkInput {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// 更新运营配置输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct UpdateOpsConfigInput {
|
||||
pub registration_enabled: Option<bool>,
|
||||
pub invite_code_required: Option<bool>,
|
||||
pub email_verification: Option<bool>,
|
||||
pub max_users: Option<i32>,
|
||||
pub max_invite_codes_per_user: Option<i32>,
|
||||
pub session_timeout_hours: Option<i32>,
|
||||
pub welcome_email: Option<bool>,
|
||||
pub system_announcements: Option<bool>,
|
||||
pub maintenance_alerts: Option<bool>,
|
||||
}
|
||||
|
||||
/// 配置更新结果类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct ConfigUpdateResultType {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub updated_settings: Vec<SettingType>,
|
||||
}
|
||||
|
||||
/// 配置验证结果类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct ConfigValidationResultType {
|
||||
pub valid: bool,
|
||||
pub errors: Vec<String>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ use rdkafka::{
|
||||
message::Message,
|
||||
};
|
||||
use serde_json;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
@ -17,25 +18,16 @@ pub struct KafkaListener {
|
||||
pub status_sender: broadcast::Sender<StatusUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ListenerError {
|
||||
KafkaError(KafkaError),
|
||||
JsonError(serde_json::Error),
|
||||
#[error("Kafka错误: {0}")]
|
||||
KafkaError(#[from] KafkaError),
|
||||
#[error("JSON错误: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
#[error("消息错误: {0}")]
|
||||
MessageError(String),
|
||||
}
|
||||
|
||||
impl From<KafkaError> for ListenerError {
|
||||
fn from(err: KafkaError) -> Self {
|
||||
ListenerError::KafkaError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ListenerError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ListenerError::JsonError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl KafkaListener {
|
||||
pub fn new(config: &Config) -> Result<(Self, broadcast::Receiver<StatusUpdate>), KafkaError> {
|
||||
let client: StreamConsumer = ClientConfig::new()
|
||||
|
||||
233
src/main.rs
233
src/main.rs
@ -10,7 +10,11 @@ mod services;
|
||||
|
||||
use app::create_router;
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Commands, MigrateArgs, ServeArgs};
|
||||
use cli::{
|
||||
AddPolicyArgs, AssignRoleArgs, CheckPermissionArgs, Cli, Commands, ListRolePermissionsArgs,
|
||||
ListUserRolesArgs, MigrateArgs, PermissionsArgs, PermissionsCommands, RemovePolicyArgs,
|
||||
RemoveRoleArgs, ServeArgs,
|
||||
};
|
||||
use config::Config;
|
||||
use db::{create_pool, run_migrations};
|
||||
use listener::KafkaListener;
|
||||
@ -25,6 +29,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
match cli.command.unwrap_or_default() {
|
||||
Commands::Serve(args) => serve_command(args).await,
|
||||
Commands::Migrate(args) => migrate_command(args).await,
|
||||
Commands::Permissions(args) => permissions_command(args).await,
|
||||
Commands::Version => version_command(),
|
||||
Commands::Config => config_command(),
|
||||
}
|
||||
@ -128,7 +133,7 @@ async fn serve_command(args: ServeArgs) -> Result<(), Box<dyn std::error::Error>
|
||||
};
|
||||
|
||||
println!("⚙️ 正在创建GraphQL路由...");
|
||||
let router = create_router(pool, config.clone(), status_sender);
|
||||
let router = create_router(pool, config.clone(), status_sender).await;
|
||||
|
||||
let bind_addr = format!("{}:{}", args.host, config.port);
|
||||
println!("🔌 正在绑定到地址: {}", bind_addr);
|
||||
@ -237,6 +242,230 @@ fn config_command() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 权限管理命令处理
|
||||
async fn permissions_command(args: PermissionsArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 加载配置
|
||||
let config = Config::from_env()?;
|
||||
|
||||
// 创建数据库连接池
|
||||
let pool = create_pool(&config.database_url).await?;
|
||||
|
||||
// 初始化 Casbin 服务
|
||||
let casbin_service = services::casbin_service::CasbinService::new(config.database_url.clone())
|
||||
.await
|
||||
.expect("Failed to initialize CasbinService");
|
||||
|
||||
println!("🔐 权限管理系统");
|
||||
println!("━━━━━━━━━━━━━━━━");
|
||||
|
||||
match args.command {
|
||||
PermissionsCommands::List => list_policies(&casbin_service).await,
|
||||
PermissionsCommands::Add(args) => add_policy(&casbin_service, args).await,
|
||||
PermissionsCommands::Remove(args) => remove_policy(&casbin_service, args).await,
|
||||
PermissionsCommands::AssignRole(args) => assign_role(&casbin_service, args).await,
|
||||
PermissionsCommands::RemoveRole(args) => remove_role(&casbin_service, args).await,
|
||||
PermissionsCommands::ListUserRoles(args) => list_user_roles(&casbin_service, args).await,
|
||||
PermissionsCommands::ListRolePermissions(args) => {
|
||||
list_role_permissions(&casbin_service, args).await
|
||||
}
|
||||
PermissionsCommands::Reload => reload_policies(&casbin_service).await,
|
||||
PermissionsCommands::Check(args) => check_permission(&casbin_service, args).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// 列出所有权限策略
|
||||
async fn list_policies(
|
||||
casbin_service: &services::casbin_service::CasbinService,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("📋 列出所有权限策略:");
|
||||
println!();
|
||||
|
||||
let policies = casbin_service.get_all_policies().await?;
|
||||
|
||||
if policies.is_empty() {
|
||||
println!(" ❌ 暂无权限策略");
|
||||
} else {
|
||||
println!(" {:<15} {:<20} {:<15}", "角色", "资源", "操作");
|
||||
println!(" ──────────────────────────────────────────────────");
|
||||
|
||||
for policy in policies {
|
||||
if policy.len() >= 3 {
|
||||
println!(" {:<15} {:<20} {:<15}", policy[0], policy[1], policy[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 添加权限策略
|
||||
async fn add_policy(
|
||||
casbin_service: &services::casbin_service::CasbinService,
|
||||
args: AddPolicyArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("➕ 添加权限策略:");
|
||||
println!(" 角色: {}", args.role);
|
||||
println!(" 资源: {}", args.resource);
|
||||
println!(" 操作: {}", args.action);
|
||||
println!();
|
||||
|
||||
casbin_service
|
||||
.add_policy(&args.role, &args.resource, &args.action)
|
||||
.await?;
|
||||
|
||||
println!(" ✅ 权限策略添加成功!");
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 移除权限策略
|
||||
async fn remove_policy(
|
||||
casbin_service: &services::casbin_service::CasbinService,
|
||||
args: RemovePolicyArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("➖ 移除权限策略:");
|
||||
println!(" 角色: {}", args.role);
|
||||
println!(" 资源: {}", args.resource);
|
||||
println!(" 操作: {}", args.action);
|
||||
println!();
|
||||
|
||||
casbin_service
|
||||
.remove_policy(&args.role, &args.resource, &args.action)
|
||||
.await?;
|
||||
|
||||
println!(" ✅ 权限策略移除成功!");
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 为用户分配角色
|
||||
async fn assign_role(
|
||||
casbin_service: &services::casbin_service::CasbinService,
|
||||
args: AssignRoleArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("👤 为用户分配角色:");
|
||||
println!(" 用户ID: {}", args.user_id);
|
||||
println!(" 角色: {}", args.role);
|
||||
println!();
|
||||
|
||||
casbin_service
|
||||
.assign_role(&args.user_id, &args.role)
|
||||
.await?;
|
||||
|
||||
println!(" ✅ 角色分配成功!");
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 移除用户角色
|
||||
async fn remove_role(
|
||||
casbin_service: &services::casbin_service::CasbinService,
|
||||
args: RemoveRoleArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🚫 移除用户角色:");
|
||||
println!(" 用户ID: {}", args.user_id);
|
||||
println!(" 角色: {}", args.role);
|
||||
println!();
|
||||
|
||||
casbin_service
|
||||
.remove_role(&args.user_id, &args.role)
|
||||
.await?;
|
||||
|
||||
println!(" ✅ 角色移除成功!");
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 列出用户角色
|
||||
async fn list_user_roles(
|
||||
casbin_service: &services::casbin_service::CasbinService,
|
||||
args: ListUserRolesArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("👤 用户角色列表:");
|
||||
println!(" 用户ID: {}", args.user_id);
|
||||
println!();
|
||||
|
||||
let roles = casbin_service.get_user_roles(&args.user_id).await?;
|
||||
|
||||
if roles.is_empty() {
|
||||
println!(" ❌ 该用户暂无角色");
|
||||
} else {
|
||||
println!(" 📋 用户角色:");
|
||||
for role in roles {
|
||||
println!(" • {}", role);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 列出角色权限
|
||||
async fn list_role_permissions(
|
||||
casbin_service: &services::casbin_service::CasbinService,
|
||||
args: ListRolePermissionsArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🔐 角色权限列表:");
|
||||
println!(" 角色: {}", args.role);
|
||||
println!();
|
||||
|
||||
let permissions = casbin_service.get_role_permissions(&args.role).await?;
|
||||
|
||||
if permissions.is_empty() {
|
||||
println!(" ❌ 该角色暂无权限");
|
||||
} else {
|
||||
println!(" 📋 角色权限:");
|
||||
println!(" {:<20} {:<15}", "资源", "操作");
|
||||
println!(" ──────────────────────────────────");
|
||||
for (resource, action) in permissions {
|
||||
println!(" {:<20} {:<15}", resource, action);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 重新加载权限策略
|
||||
async fn reload_policies(
|
||||
casbin_service: &services::casbin_service::CasbinService,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🔄 重新加载权限策略:");
|
||||
println!();
|
||||
|
||||
casbin_service.reload_policy().await?;
|
||||
|
||||
println!(" ✅ 权限策略重载成功!");
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查用户权限
|
||||
async fn check_permission(
|
||||
casbin_service: &services::casbin_service::CasbinService,
|
||||
args: CheckPermissionArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🔍 检查用户权限:");
|
||||
println!(" 用户ID: {}", args.user_id);
|
||||
println!(" 资源: {}", args.resource);
|
||||
println!(" 操作: {}", args.action);
|
||||
println!();
|
||||
|
||||
let has_permission = casbin_service
|
||||
.check_permission(&args.user_id, &args.resource, &args.action)
|
||||
.await?;
|
||||
|
||||
if has_permission {
|
||||
println!(" ✅ 用户有权限执行此操作");
|
||||
} else {
|
||||
println!(" ❌ 用户无权限执行此操作");
|
||||
}
|
||||
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mask_database_url(url: &str) -> String {
|
||||
if let Some(at_pos) = url.find('@') {
|
||||
if let Some(scheme_end) = url.find("://") {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
pub mod invite_code;
|
||||
pub mod kafka_message;
|
||||
pub mod page_block;
|
||||
pub mod settings;
|
||||
pub mod user;
|
||||
|
||||
pub use invite_code::*;
|
||||
pub use kafka_message::*;
|
||||
pub use page_block::*;
|
||||
pub use settings::*;
|
||||
pub use user::*;
|
||||
|
||||
621
src/models/page_block.rs
Normal file
621
src/models/page_block.rs
Normal file
@ -0,0 +1,621 @@
|
||||
use async_graphql::{InputObject, SimpleObject, Union};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 页面模型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
pub struct Page {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub created_by: Option<Uuid>,
|
||||
pub updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
/// 页面块联合类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Union)]
|
||||
pub enum Block {
|
||||
TextBlock(TextBlock),
|
||||
ChartBlock(ChartBlock),
|
||||
SettingsBlock(SettingsBlock),
|
||||
TableBlock(TableBlock),
|
||||
HeroBlock(HeroBlock),
|
||||
}
|
||||
|
||||
/// 文本块
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
pub struct TextBlock {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub markdown: String,
|
||||
pub is_active: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 图表块
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
pub struct ChartBlock {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: String,
|
||||
pub chart_type: String, // line, bar, pie, etc.
|
||||
pub series: Vec<DataPoint>,
|
||||
pub config: Option<serde_json::Value>, // 图表配置
|
||||
pub is_active: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 数据点
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
pub struct DataPoint {
|
||||
pub id: Uuid,
|
||||
pub chart_block_id: Uuid,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub label: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
/// 设置块
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
pub struct SettingsBlock {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub category: String,
|
||||
pub editable: bool,
|
||||
pub display_mode: String, // form, table, cards, etc.
|
||||
pub is_active: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 表格块
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
pub struct TableBlock {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub columns: Vec<TableColumn>,
|
||||
pub data_source: String, // 数据源类型
|
||||
pub data_config: Option<serde_json::Value>, // 数据源配置
|
||||
pub is_active: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 表格列定义
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
pub struct TableColumn {
|
||||
pub id: Uuid,
|
||||
pub table_block_id: Uuid,
|
||||
pub name: String,
|
||||
pub label: String,
|
||||
pub data_type: String, // string, number, boolean, date, etc.
|
||||
pub is_sortable: bool,
|
||||
pub is_filterable: bool,
|
||||
pub width: Option<i32>,
|
||||
pub order: i32,
|
||||
}
|
||||
|
||||
/// 英雄块(用于页面头部展示)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
pub struct HeroBlock {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: String,
|
||||
pub subtitle: Option<String>,
|
||||
pub background_image: Option<String>,
|
||||
pub background_color: Option<String>,
|
||||
pub text_color: Option<String>,
|
||||
pub cta_text: Option<String>,
|
||||
pub cta_link: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// 创建页面输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreatePageInput {
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 更新页面输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct UpdatePageInput {
|
||||
pub title: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 创建文本块输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateTextBlockInput {
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub markdown: String,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 创建图表块输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateChartBlockInput {
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: String,
|
||||
pub chart_type: String,
|
||||
pub series: Vec<CreateDataPointInput>,
|
||||
pub config: Option<serde_json::Value>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 创建数据点输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateDataPointInput {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub label: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
// 创建设置块输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateSettingsBlockInput {
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub category: String,
|
||||
pub editable: bool,
|
||||
pub display_mode: String,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 页面过滤器
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct PageFilterInput {
|
||||
pub title: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
/// 创建新页面
|
||||
pub fn new(input: CreatePageInput, user_id: Uuid) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
title: input.title,
|
||||
slug: input.slug,
|
||||
description: input.description,
|
||||
is_active: input.is_active.unwrap_or(true),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
created_by: Some(user_id),
|
||||
updated_by: Some(user_id),
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新页面
|
||||
pub fn update(&mut self, input: UpdatePageInput, user_id: Uuid) {
|
||||
if let Some(title) = input.title {
|
||||
self.title = title;
|
||||
}
|
||||
if let Some(slug) = input.slug {
|
||||
self.slug = slug;
|
||||
}
|
||||
if let Some(description) = input.description {
|
||||
self.description = Some(description);
|
||||
}
|
||||
if let Some(is_active) = input.is_active {
|
||||
self.is_active = is_active;
|
||||
}
|
||||
self.updated_at = Utc::now();
|
||||
self.updated_by = Some(user_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl TextBlock {
|
||||
/// 创建新文本块
|
||||
pub fn new(input: CreateTextBlockInput) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
page_id: input.page_id,
|
||||
block_order: input.block_order,
|
||||
title: input.title,
|
||||
markdown: input.markdown,
|
||||
is_active: input.is_active.unwrap_or(true),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChartBlock {
|
||||
/// 创建新图表块
|
||||
pub fn new(input: CreateChartBlockInput) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
page_id: input.page_id,
|
||||
block_order: input.block_order,
|
||||
title: input.title,
|
||||
chart_type: input.chart_type,
|
||||
series: input
|
||||
.series
|
||||
.into_iter()
|
||||
.map(|dp| DataPoint {
|
||||
id: Uuid::new_v4(),
|
||||
chart_block_id: Uuid::new_v4(), // 临时ID,保存时会更新
|
||||
x: dp.x,
|
||||
y: dp.y,
|
||||
label: dp.label,
|
||||
color: dp.color,
|
||||
})
|
||||
.collect(),
|
||||
config: input.config,
|
||||
is_active: input.is_active.unwrap_or(true),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsBlock {
|
||||
/// 创建新设置块
|
||||
pub fn new(input: CreateSettingsBlockInput) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
page_id: input.page_id,
|
||||
block_order: input.block_order,
|
||||
title: input.title,
|
||||
category: input.category,
|
||||
editable: input.editable,
|
||||
display_mode: input.display_mode,
|
||||
is_active: input.is_active.unwrap_or(true),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建表格块输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateTableBlockInput {
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub columns: Vec<CreateTableColumnInput>,
|
||||
pub data_source: String,
|
||||
pub data_config: Option<serde_json::Value>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 创建表格列输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateTableColumnInput {
|
||||
pub name: String,
|
||||
pub label: String,
|
||||
pub data_type: String,
|
||||
pub is_sortable: bool,
|
||||
pub is_filterable: bool,
|
||||
pub width: Option<i32>,
|
||||
pub order: i32,
|
||||
}
|
||||
|
||||
// 创建英雄块输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateHeroBlockInput {
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: String,
|
||||
pub subtitle: Option<String>,
|
||||
pub background_image: Option<String>,
|
||||
pub background_color: Option<String>,
|
||||
pub text_color: Option<String>,
|
||||
pub cta_text: Option<String>,
|
||||
pub cta_link: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 更新文本块输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct UpdateTextBlockInput {
|
||||
pub title: Option<String>,
|
||||
pub markdown: Option<String>,
|
||||
pub block_order: Option<i32>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 更新图表块输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct UpdateChartBlockInput {
|
||||
pub title: Option<String>,
|
||||
pub chart_type: Option<String>,
|
||||
pub config: Option<serde_json::Value>,
|
||||
pub block_order: Option<i32>,
|
||||
pub is_active: Option<bool>,
|
||||
pub series: Option<Vec<CreateDataPointInput>>,
|
||||
}
|
||||
|
||||
// 更新设置块输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct UpdateSettingsBlockInput {
|
||||
pub title: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub editable: Option<bool>,
|
||||
pub display_mode: Option<String>,
|
||||
pub block_order: Option<i32>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 更新表格块输入
|
||||
#[derive(Debug, Clone, Deserialize, InputObject)]
|
||||
pub struct UpdateTableBlockInput {
|
||||
pub title: Option<String>,
|
||||
pub data_source: Option<String>,
|
||||
pub data_config: Option<serde_json::Value>,
|
||||
pub block_order: Option<i32>,
|
||||
pub is_active: Option<bool>,
|
||||
pub columns: Option<Vec<CreateTableColumnInput>>,
|
||||
}
|
||||
|
||||
// 更新英雄块输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct UpdateHeroBlockInput {
|
||||
pub title: Option<String>,
|
||||
pub subtitle: Option<String>,
|
||||
pub background_image: Option<String>,
|
||||
pub background_color: Option<String>,
|
||||
pub text_color: Option<String>,
|
||||
pub cta_text: Option<String>,
|
||||
pub cta_link: Option<String>,
|
||||
pub block_order: Option<i32>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
impl TableBlock {
|
||||
/// 创建新表格块
|
||||
pub fn new(input: CreateTableBlockInput) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
page_id: input.page_id,
|
||||
block_order: input.block_order,
|
||||
title: input.title,
|
||||
columns: input
|
||||
.columns
|
||||
.into_iter()
|
||||
.map(|col| TableColumn {
|
||||
id: Uuid::new_v4(),
|
||||
table_block_id: Uuid::new_v4(), // 临时ID,保存时会更新
|
||||
name: col.name,
|
||||
label: col.label,
|
||||
data_type: col.data_type,
|
||||
is_sortable: col.is_sortable,
|
||||
is_filterable: col.is_filterable,
|
||||
width: col.width,
|
||||
order: col.order,
|
||||
})
|
||||
.collect(),
|
||||
data_source: input.data_source,
|
||||
data_config: input.data_config,
|
||||
is_active: input.is_active.unwrap_or(true),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新表格块
|
||||
pub fn update(&mut self, input: UpdateTableBlockInput) {
|
||||
if let Some(title) = input.title {
|
||||
self.title = Some(title);
|
||||
}
|
||||
if let Some(data_source) = input.data_source {
|
||||
self.data_source = data_source;
|
||||
}
|
||||
if let Some(data_config) = input.data_config {
|
||||
self.data_config = Some(data_config);
|
||||
}
|
||||
if let Some(block_order) = input.block_order {
|
||||
self.block_order = block_order;
|
||||
}
|
||||
if let Some(is_active) = input.is_active {
|
||||
self.is_active = is_active;
|
||||
}
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
impl HeroBlock {
|
||||
/// 创建新英雄块
|
||||
pub fn new(input: CreateHeroBlockInput) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
page_id: input.page_id,
|
||||
block_order: input.block_order,
|
||||
title: input.title,
|
||||
subtitle: input.subtitle,
|
||||
background_image: input.background_image,
|
||||
background_color: input.background_color,
|
||||
text_color: input.text_color,
|
||||
cta_text: input.cta_text,
|
||||
cta_link: input.cta_link,
|
||||
is_active: input.is_active.unwrap_or(true),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新英雄块
|
||||
pub fn update(&mut self, input: UpdateHeroBlockInput) {
|
||||
if let Some(title) = input.title {
|
||||
self.title = title;
|
||||
}
|
||||
if let Some(subtitle) = input.subtitle {
|
||||
self.subtitle = Some(subtitle);
|
||||
}
|
||||
if let Some(background_image) = input.background_image {
|
||||
self.background_image = Some(background_image);
|
||||
}
|
||||
if let Some(background_color) = input.background_color {
|
||||
self.background_color = Some(background_color);
|
||||
}
|
||||
if let Some(text_color) = input.text_color {
|
||||
self.text_color = Some(text_color);
|
||||
}
|
||||
if let Some(cta_text) = input.cta_text {
|
||||
self.cta_text = Some(cta_text);
|
||||
}
|
||||
if let Some(cta_link) = input.cta_link {
|
||||
self.cta_link = Some(cta_link);
|
||||
}
|
||||
if let Some(block_order) = input.block_order {
|
||||
self.block_order = block_order;
|
||||
}
|
||||
if let Some(is_active) = input.is_active {
|
||||
self.is_active = is_active;
|
||||
}
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
impl TextBlock {
|
||||
/// 更新文本块
|
||||
pub fn update(&mut self, input: UpdateTextBlockInput) {
|
||||
if let Some(title) = input.title {
|
||||
self.title = Some(title);
|
||||
}
|
||||
if let Some(markdown) = input.markdown {
|
||||
self.markdown = markdown;
|
||||
}
|
||||
if let Some(block_order) = input.block_order {
|
||||
self.block_order = block_order;
|
||||
}
|
||||
if let Some(is_active) = input.is_active {
|
||||
self.is_active = is_active;
|
||||
}
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
impl ChartBlock {
|
||||
/// 更新图表块
|
||||
pub fn update(&mut self, input: UpdateChartBlockInput) {
|
||||
if let Some(title) = input.title {
|
||||
self.title = title;
|
||||
}
|
||||
if let Some(chart_type) = input.chart_type {
|
||||
self.chart_type = chart_type;
|
||||
}
|
||||
if let Some(config) = input.config {
|
||||
self.config = Some(config);
|
||||
}
|
||||
if let Some(block_order) = input.block_order {
|
||||
self.block_order = block_order;
|
||||
}
|
||||
if let Some(is_active) = input.is_active {
|
||||
self.is_active = is_active;
|
||||
}
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsBlock {
|
||||
/// 更新设置块
|
||||
pub fn update(&mut self, input: UpdateSettingsBlockInput) {
|
||||
if let Some(title) = input.title {
|
||||
self.title = Some(title);
|
||||
}
|
||||
if let Some(category) = input.category {
|
||||
self.category = category;
|
||||
}
|
||||
if let Some(editable) = input.editable {
|
||||
self.editable = editable;
|
||||
}
|
||||
if let Some(display_mode) = input.display_mode {
|
||||
self.display_mode = display_mode;
|
||||
}
|
||||
if let Some(block_order) = input.block_order {
|
||||
self.block_order = block_order;
|
||||
}
|
||||
if let Some(is_active) = input.is_active {
|
||||
self.is_active = is_active;
|
||||
}
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
// 页面块过滤器
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct BlockFilterInput {
|
||||
pub page_id: Option<Uuid>,
|
||||
pub block_type: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
// 页面块排序
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct BlockSortInput {
|
||||
pub field: String,
|
||||
pub direction: String, // "asc" or "desc"
|
||||
}
|
||||
|
||||
// 分页输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct PaginationInput {
|
||||
pub page: Option<i32>,
|
||||
pub per_page: Option<i32>,
|
||||
}
|
||||
|
||||
// 分页结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct PaginatedResult<T: async_graphql::OutputType + Send + Sync> {
|
||||
pub items: Vec<T>,
|
||||
pub total: i64,
|
||||
pub page: i32,
|
||||
pub per_page: i32,
|
||||
pub total_pages: i32,
|
||||
}
|
||||
|
||||
impl<T: async_graphql::OutputType + Send + Sync> PaginatedResult<T> {
|
||||
pub fn new(items: Vec<T>, total: i64, page: i32, per_page: i32) -> Self {
|
||||
let total_pages = ((total as f64) / (per_page as f64)).ceil() as i32;
|
||||
Self {
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
per_page,
|
||||
total_pages,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 页面统计信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct PageStats {
|
||||
pub text_blocks: i32,
|
||||
pub chart_blocks: i32,
|
||||
pub settings_blocks: i32,
|
||||
pub table_blocks: i32,
|
||||
pub hero_blocks: i32,
|
||||
pub total_blocks: i32,
|
||||
}
|
||||
252
src/services/casbin_service.rs
Normal file
252
src/services/casbin_service.rs
Normal file
@ -0,0 +1,252 @@
|
||||
use anyhow::{Context, Result};
|
||||
use casbin::{DefaultModel, Enforcer, MgmtApi};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::info;
|
||||
|
||||
use sqlx_adapter::casbin::prelude::*;
|
||||
use sqlx_adapter::SqlxAdapter;
|
||||
|
||||
pub struct CasbinService {
|
||||
enforcer: Arc<RwLock<Enforcer>>,
|
||||
}
|
||||
|
||||
impl CasbinService {
|
||||
pub async fn new(url: String) -> Result<Self> {
|
||||
let adapter = SqlxAdapter::new(url, 8).await?;
|
||||
|
||||
// 定义 RBAC 模型
|
||||
let model = r#"
|
||||
[request_definition]
|
||||
r = sub, obj, act
|
||||
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
|
||||
[role_definition]
|
||||
g = _, _
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
|
||||
"#;
|
||||
|
||||
let model = DefaultModel::from_str(model)
|
||||
.await
|
||||
.context("Failed to parse casbin model")?;
|
||||
|
||||
// 创建 enforcer
|
||||
let enforcer = Enforcer::new(model, adapter).await?;
|
||||
|
||||
info!("Casbin service initialized successfully");
|
||||
|
||||
Ok(Self {
|
||||
enforcer: Arc::new(RwLock::new(enforcer)),
|
||||
})
|
||||
}
|
||||
|
||||
/// 检查用户是否有权限执行某个操作
|
||||
pub async fn check_permission(
|
||||
&self,
|
||||
user_id: &str,
|
||||
resource: &str,
|
||||
action: &str,
|
||||
) -> Result<bool> {
|
||||
let enforcer = self.enforcer.read().await;
|
||||
|
||||
// 首先检查用户是否有直接权限
|
||||
let has_permission = enforcer
|
||||
.enforce((user_id, resource, action))
|
||||
.context("Failed to check permission")?;
|
||||
|
||||
if has_permission {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// 如果直接权限检查失败,检查用户角色权限
|
||||
let roles = self.get_user_roles(user_id).await?;
|
||||
|
||||
for role in roles {
|
||||
let has_role_permission = enforcer
|
||||
.enforce((&role, resource, action))
|
||||
.context("Failed to check role permission")?;
|
||||
|
||||
if has_role_permission {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// 获取用户的所有角色
|
||||
pub async fn get_user_roles(&self, user_id: &str) -> Result<Vec<String>> {
|
||||
let enforcer = self.enforcer.read().await;
|
||||
|
||||
let roles = enforcer.get_roles_for_user(user_id, None);
|
||||
|
||||
Ok(roles.into_iter().map(|r| r.to_string()).collect())
|
||||
}
|
||||
|
||||
/// 为用户分配角色
|
||||
pub async fn assign_role(&self, user_id: &str, role: &str) -> Result<()> {
|
||||
let mut enforcer = self.enforcer.write().await;
|
||||
|
||||
enforcer
|
||||
.add_role_for_user(user_id, role, None)
|
||||
.await
|
||||
.context("Failed to assign role to user")?;
|
||||
|
||||
// 保存到数据库
|
||||
enforcer
|
||||
.save_policy()
|
||||
.await
|
||||
.context("Failed to save policy")?;
|
||||
|
||||
info!("Role {} assigned to user {}", role, user_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 移除用户的角色
|
||||
pub async fn remove_role(&self, user_id: &str, role: &str) -> Result<()> {
|
||||
let mut enforcer = self.enforcer.write().await;
|
||||
|
||||
enforcer
|
||||
.delete_role_for_user(user_id, role, None)
|
||||
.await
|
||||
.context("Failed to remove role from user")?;
|
||||
|
||||
// 保存到数据库
|
||||
enforcer
|
||||
.save_policy()
|
||||
.await
|
||||
.context("Failed to save policy")?;
|
||||
|
||||
info!("Role {} removed from user {}", role, user_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 添加权限策略
|
||||
pub async fn add_policy(&self, role: &str, resource: &str, action: &str) -> Result<()> {
|
||||
let mut enforcer = self.enforcer.write().await;
|
||||
|
||||
enforcer
|
||||
.add_policy(vec![
|
||||
role.to_string(),
|
||||
resource.to_string(),
|
||||
action.to_string(),
|
||||
])
|
||||
.await
|
||||
.context("Failed to add policy")?;
|
||||
|
||||
// 保存到数据库
|
||||
enforcer
|
||||
.save_policy()
|
||||
.await
|
||||
.context("Failed to save policy")?;
|
||||
|
||||
info!("Policy added: {} {} {}", role, resource, action);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 移除权限策略
|
||||
pub async fn remove_policy(&self, role: &str, resource: &str, action: &str) -> Result<()> {
|
||||
let mut enforcer = self.enforcer.write().await;
|
||||
|
||||
enforcer
|
||||
.remove_policy(vec![
|
||||
role.to_string(),
|
||||
resource.to_string(),
|
||||
action.to_string(),
|
||||
])
|
||||
.await
|
||||
.context("Failed to remove policy")?;
|
||||
|
||||
// 保存到数据库
|
||||
enforcer
|
||||
.save_policy()
|
||||
.await
|
||||
.context("Failed to save policy")?;
|
||||
|
||||
info!("Policy removed: {} {} {}", role, resource, action);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取所有策略
|
||||
pub async fn get_all_policies(&self) -> Result<Vec<Vec<String>>> {
|
||||
let enforcer = self.enforcer.read().await;
|
||||
|
||||
let policies = enforcer.get_policy();
|
||||
|
||||
Ok(policies
|
||||
.into_iter()
|
||||
.map(|p| p.into_iter().map(|s| s.to_string()).collect())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// 获取角色的所有权限
|
||||
pub async fn get_role_permissions(&self, role: &str) -> Result<Vec<(String, String)>> {
|
||||
let enforcer = self.enforcer.read().await;
|
||||
|
||||
let policies = enforcer.get_filtered_policy(0, vec![role.to_string()]);
|
||||
|
||||
Ok(policies
|
||||
.into_iter()
|
||||
.map(|p| (p[1].to_string(), p[2].to_string()))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// 重新加载策略
|
||||
pub async fn reload_policy(&self) -> Result<()> {
|
||||
let mut enforcer = self.enforcer.write().await;
|
||||
|
||||
enforcer
|
||||
.load_policy()
|
||||
.await
|
||||
.context("Failed to reload policy")?;
|
||||
|
||||
info!("Policy reloaded successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查用户是否有特定资源的读取权限
|
||||
pub async fn can_read(&self, user_id: &str, resource: &str) -> Result<bool> {
|
||||
self.check_permission(user_id, resource, "read").await
|
||||
}
|
||||
|
||||
/// 检查用户是否有特定资源的写入权限
|
||||
pub async fn can_write(&self, user_id: &str, resource: &str) -> Result<bool> {
|
||||
self.check_permission(user_id, resource, "write").await
|
||||
}
|
||||
|
||||
/// 检查用户是否有特定资源的删除权限
|
||||
pub async fn can_delete(&self, user_id: &str, resource: &str) -> Result<bool> {
|
||||
self.check_permission(user_id, resource, "delete").await
|
||||
}
|
||||
|
||||
/// 批量检查权限
|
||||
pub async fn check_permissions(
|
||||
&self,
|
||||
user_id: &str,
|
||||
permissions: &[(&str, &str)],
|
||||
) -> Result<Vec<bool>> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for (resource, action) in permissions {
|
||||
let has_permission = self.check_permission(user_id, resource, action).await?;
|
||||
results.push(has_permission);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for CasbinService {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
enforcer: Arc::clone(&self.enforcer),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
pub mod casbin_service;
|
||||
pub mod invite_code_service;
|
||||
pub mod mosaic_service;
|
||||
pub mod page_block_service;
|
||||
pub mod query_builder;
|
||||
pub mod settings_manager;
|
||||
pub mod settings_service;
|
||||
|
||||
1546
src/services/page_block_service.rs
Normal file
1546
src/services/page_block_service.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -54,6 +54,11 @@ impl SettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取数据库连接池的引用(用于事务)
|
||||
pub fn get_pool(&self) -> &PgPool {
|
||||
&self.pool
|
||||
}
|
||||
|
||||
/// 创建新的配置项
|
||||
pub async fn create_setting(
|
||||
&self,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user