add casbin
Some checks are pending
Docker Build and Push / build (push) Waiting to run

This commit is contained in:
tsuki 2025-08-11 21:26:29 +08:00
parent d4448c6129
commit d29679c6f8
23 changed files with 6778 additions and 172 deletions

365
Cargo.lock generated
View File

@ -45,6 +45,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"const-random",
"getrandom 0.3.3", "getrandom 0.3.3",
"once_cell", "once_cell",
"version_check", "version_check",
@ -870,6 +871,31 @@ dependencies = [
"serde", "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]] [[package]]
name = "cc" name = "cc"
version = "1.2.30" version = "1.2.30"
@ -1008,6 +1034,26 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "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]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@ -1058,6 +1104,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-queue" name = "crossbeam-queue"
version = "0.3.12" version = "0.3.12"
@ -1073,6 +1128,12 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@ -1201,6 +1262,27 @@ dependencies = [
"subtle", "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]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@ -1336,6 +1418,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fixedbitset"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]] [[package]]
name = "flume" name = "flume"
version = "0.11.1" version = "0.11.1"
@ -1344,7 +1432,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"spin", "spin 0.9.8",
] ]
[[package]] [[package]]
@ -1653,6 +1741,15 @@ dependencies = [
"foldhash", "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]] [[package]]
name = "hashlink" name = "hashlink"
version = "0.10.0" version = "0.10.0"
@ -2089,6 +2186,17 @@ dependencies = [
"serde", "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]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@ -2160,7 +2268,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [ dependencies = [
"spin", "spin 0.9.8",
] ]
[[package]] [[package]]
@ -2216,6 +2324,16 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 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]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.30.1" version = "0.30.1"
@ -2310,6 +2428,7 @@ dependencies = [
"axum-jwt-auth", "axum-jwt-auth",
"axum-reverse-proxy", "axum-reverse-proxy",
"axum_gcra", "axum_gcra",
"casbin",
"chrono", "chrono",
"clap", "clap",
"dotenvy", "dotenvy",
@ -2323,6 +2442,8 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"sqlx-adapter",
"thiserror 2.0.12",
"tokio", "tokio",
"tower 0.4.13", "tower 0.4.13",
"tower-http 0.5.2", "tower-http 0.5.2",
@ -2374,6 +2495,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 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]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.2.1" version = "0.2.1"
@ -2413,7 +2544,7 @@ dependencies = [
"httparse", "httparse",
"memchr", "memchr",
"mime", "mime",
"spin", "spin 0.9.8",
"version_check", "version_check",
] ]
@ -2447,6 +2578,15 @@ dependencies = [
"memoffset", "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]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -2575,6 +2715,9 @@ name = "once_cell"
version = "1.21.3" version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "once_cell_polyfill" name = "once_cell_polyfill"
@ -2741,6 +2884,16 @@ dependencies = [
"sha2", "sha2",
] ]
[[package]]
name = "petgraph"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@ -2821,6 +2974,12 @@ dependencies = [
"windows-sys 0.60.2", "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]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.2" version = "0.1.2"
@ -3082,6 +3241,17 @@ dependencies = [
"bitflags 2.9.1", "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]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.1"
@ -3158,6 +3328,36 @@ dependencies = [
"webpki-roots", "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]] [[package]]
name = "ring" name = "ring"
version = "0.17.14" version = "0.17.14"
@ -3357,6 +3557,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 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]] [[package]]
name = "scc" name = "scc"
version = "2.3.4" version = "2.3.4"
@ -3613,6 +3822,37 @@ version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" 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]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"
@ -3622,6 +3862,18 @@ dependencies = [
"serde", "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]] [[package]]
name = "socket2" name = "socket2"
version = "0.4.10" version = "0.4.10"
@ -3652,6 +3904,12 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
@ -3684,6 +3942,18 @@ dependencies = [
"sqlx-sqlite", "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]] [[package]]
name = "sqlx-core" name = "sqlx-core"
version = "0.8.6" version = "0.8.6"
@ -3705,7 +3975,7 @@ dependencies = [
"futures-io", "futures-io",
"futures-util", "futures-util",
"hashbrown 0.15.4", "hashbrown 0.15.4",
"hashlink", "hashlink 0.10.0",
"indexmap", "indexmap",
"ipnetwork", "ipnetwork",
"log", "log",
@ -3721,6 +3991,8 @@ dependencies = [
"smallvec", "smallvec",
"thiserror 2.0.12", "thiserror 2.0.12",
"time", "time",
"tokio",
"tokio-stream",
"tracing", "tracing",
"url", "url",
"uuid", "uuid",
@ -3761,6 +4033,7 @@ dependencies = [
"sqlx-postgres", "sqlx-postgres",
"sqlx-sqlite", "sqlx-sqlite",
"syn 2.0.104", "syn 2.0.104",
"tokio",
"url", "url",
] ]
@ -3902,6 +4175,12 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "static_assertions_next" name = "static_assertions_next"
version = "1.1.2" version = "1.1.2"
@ -3995,6 +4274,12 @@ dependencies = [
"syn 2.0.104", "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]] [[package]]
name = "tap" name = "tap"
version = "1.0.1" version = "1.0.1"
@ -4014,6 +4299,26 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@ -4094,6 +4399,15 @@ dependencies = [
"time-core", "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]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.1" version = "0.8.1"
@ -4519,6 +4833,16 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" 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]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@ -4620,6 +4944,30 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.77" version = "0.3.77"
@ -4687,6 +5035,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"

View File

@ -51,4 +51,8 @@ rdkafka = "0.38.0"
axum_gcra = "0.1.1" axum_gcra = "0.1.1"
lettre = "0.11.18" lettre = "0.11.18"
anyhow = "1.0.98" 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
View File

@ -1,119 +1,201 @@
# 瓦片服务器 (Tile Server) # Map Application Server (MAPP)
基于 Axum 的高性能瓦片服务器,支持通过时间参数动态加载不同时次的瓦片数据 一个基于 GraphQL 的现代地图应用服务器,支持实时数据同步、权限管理和多租户架构
## 功能特性 ## 功能特性
- ✅ 支持时间参数动态加载瓦片 - 🗺️ 基于 GraphQL 的地图数据服务
- ✅ 标准 XYZ 瓦片格式支持 - 🔐 基于 Casbin 的 RBAC 权限管理
- ✅ 高性能异步文件读取 - 📡 Kafka 消息队列集成
- ✅ 适当的 HTTP 缓存头设置 - 🗄️ PostgreSQL 数据库支持
- ✅ 完整的错误处理 - 🚀 高性能异步架构
- ✅ CORS 支持 - 🔧 完整的 CLI 管理工具
## 目录结构
```
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"
```
## 快速开始 ## 快速开始
### 1. 编译运行 ### 环境要求
- Rust 1.70+
- PostgreSQL 12+
- Kafka 2.8+ (可选)
### 安装
```bash ```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 ```bash
curl http://localhost:3050/ # 启动服务器
./target/release/mapp serve
# 开发模式
./target/release/mapp serve --dev --verbose
# 运行数据库迁移
./target/release/mapp migrate
``` ```
获取示例瓦片: ## 权限管理 CLI
MAPP 提供了完整的命令行权限管理工具,基于 Casbin RBAC 模型:
### 基本用法
```bash ```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 ```bash
mkdir -p tiles/202507220000/6/42 # 添加权限策略:给 admin 角色添加删除用户的权限
# 将您的瓦片文件放入相应目录 ./target/release/mapp permissions add --role admin --resource users --action delete
cp your-tile.png tiles/202507220000/6/42/20.png
# 移除权限策略:移除 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` ```bash
- 时间格式无效: `400 Bad Request` # 运行所有迁移
- 瓦片不存在: `404 Not Found` ./target/release/mapp migrate
- 服务器错误: `500 Internal Server Error`
## 开发说明 # 查看迁移状态
./target/release/mapp migrate --dry-run
# 强制重新运行迁移
./target/release/mapp migrate --force
```
## 开发
### 项目结构 ### 项目结构
``` ```
src/ src/
└── main.rs # 主服务器代码 ├── app.rs # 应用路由配置
Cargo.toml # 依赖配置 ├── auth.rs # 认证和授权
README.md # 项目说明 ├── cli.rs # 命令行接口
tiles/ # 瓦片数据目录 ├── config.rs # 配置管理
├── db.rs # 数据库连接
├── graphql/ # GraphQL 相关
│ ├── guards.rs # 权限守卫
│ ├── mutation.rs # 变更操作
│ ├── query.rs # 查询操作
│ └── types.rs # 类型定义
├── listener/ # 消息监听器
├── models/ # 数据模型
└── services/ # 业务服务
└── casbin_service.rs # 权限管理服务
``` ```
### 核心依赖 ### 构建
- `axum`: Web 框架 ```bash
- `tokio`: 异步运行时 # 开发构建
- `tower-http`: HTTP 中间件 (CORS) cargo build
- `tracing`: 日志记录
### 扩展建议 # 发布构建
cargo build --release
- 添加瓦片格式验证 # 运行测试
- 实现瓦片缓存机制 cargo test
- 支持多种图像格式
- 添加访问日志 # 代码检查
- 实现配置文件支持 cargo clippy
```
## 许可证
本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件。
## 贡献
欢迎提交 Issue 和 Pull Request
## 支持
如有问题,请通过以下方式联系:
- 提交 GitHub Issue
- 发送邮件至 [support@example.com]

View 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;

View 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);

View 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)';

View 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();

View 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;

View 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;

View File

@ -24,9 +24,10 @@ use crate::{
config::Config, config::Config,
graphql::{subscription::StatusUpdate, MutationRoot, QueryRoot, SubscriptionRoot}, graphql::{subscription::StatusUpdate, MutationRoot, QueryRoot, SubscriptionRoot},
services::{ services::{
invite_code_service::InviteCodeService, mosaic_service::MosaicService, casbin_service::CasbinService, invite_code_service::InviteCodeService,
system_config_service::SystemConfigService, user_service::UserService, mosaic_service::MosaicService, page_block_service::PageBlockService,
settings_service::SettingsService, 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 status_sender: Option<broadcast::Sender<StatusUpdate>>,
} }
pub fn create_router( pub async fn create_router(
pool: PgPool, pool: PgPool,
config: Config, config: Config,
status_sender: Option<broadcast::Sender<StatusUpdate>>, status_sender: Option<broadcast::Sender<StatusUpdate>>,
@ -52,6 +53,11 @@ pub fn create_router(
let system_config_service = SystemConfigService::new(pool.clone()); let system_config_service = SystemConfigService::new(pool.clone());
let mosaic_service = MosaicService::new(pool.clone()); let mosaic_service = MosaicService::new(pool.clone());
let settings_service = SettingsService::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) let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
.data(pool) .data(pool)
@ -60,6 +66,8 @@ pub fn create_router(
.data(system_config_service) .data(system_config_service)
.data(mosaic_service) .data(mosaic_service)
.data(settings_service) .data(settings_service)
.data(page_block_service)
.data(casbin_service)
.data(config.clone()) .data(config.clone())
.data(status_sender.clone()) .data(status_sender.clone())
.finish(); .finish();
@ -94,7 +102,6 @@ pub fn create_router(
// (Method::POST, "/graphql"), // (Method::POST, "/graphql"),
// Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()), // Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()),
// ) // )
// .with_global_fallback(false)
// .with_gc_interval(1000) // .with_gc_interval(1000)
// .default_handle_error(), // .default_handle_error(),
// ) // )

View File

@ -16,6 +16,8 @@ pub enum Commands {
Serve(ServeArgs), Serve(ServeArgs),
/// 运行数据库迁移 /// 运行数据库迁移
Migrate(MigrateArgs), Migrate(MigrateArgs),
/// 权限管理
Permissions(PermissionsArgs),
/// 显示版本信息 /// 显示版本信息
Version, Version,
/// 显示配置信息 /// 显示配置信息
@ -60,6 +62,124 @@ pub struct MigrateArgs {
pub force: bool, 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 { impl Default for Commands {
fn default() -> Self { fn default() -> Self {
Commands::Serve(ServeArgs { Commands::Serve(ServeArgs {

View File

@ -1,6 +1,7 @@
use crate::auth::get_auth_user;
use crate::models::user::Role; use crate::models::user::Role;
use crate::{auth::get_auth_user, services::casbin_service::CasbinService};
use async_graphql::{Context, Error, Guard, Result}; use async_graphql::{Context, Error, Guard, Result};
use tracing::warn;
pub struct RequireRole(pub Role); 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

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,6 @@
use crate::models::settings::{ use crate::models::{user::Role, Setting};
CreateSetting, Setting, SettingFilter, SettingHistory, UpdateSetting, use async_graphql::{InputObject, SimpleObject, Union};
}; use serde::{Deserialize, Serialize};
use crate::models::user::Role;
use async_graphql::{InputObject, SimpleObject};
use serde_json::Value; use serde_json::Value;
use uuid::Uuid; use uuid::Uuid;
@ -77,7 +75,7 @@ pub struct UserInfoRespnose {
} }
// Settings GraphQL types // Settings GraphQL types
#[derive(SimpleObject)] #[derive(SimpleObject, Debug, Clone)]
pub struct SettingType { pub struct SettingType {
pub id: Uuid, pub id: Uuid,
pub key: String, pub key: String,
@ -94,6 +92,26 @@ pub struct SettingType {
pub updated_by: Option<Uuid>, 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)] #[derive(InputObject)]
pub struct CreateSettingInput { pub struct CreateSettingInput {
pub key: String, pub key: String,
@ -139,13 +157,545 @@ pub struct SettingsStatsType {
pub stats: std::collections::HashMap<String, i64>, 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)] #[derive(InputObject)]
pub struct BatchUpdateSettingsInput { pub struct CreatePageInputType {
pub updates: Vec<BatchUpdateItemInput>, pub title: String,
pub slug: String,
pub description: Option<String>,
pub is_active: Option<bool>,
} }
#[derive(InputObject)] #[derive(InputObject)]
pub struct BatchUpdateItemInput { pub struct UpdatePageInputType {
pub key: String, pub title: Option<String>,
pub value: Value, 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>,
} }

View File

@ -5,6 +5,7 @@ use rdkafka::{
message::Message, message::Message,
}; };
use serde_json; use serde_json;
use thiserror::Error;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
@ -17,25 +18,16 @@ pub struct KafkaListener {
pub status_sender: broadcast::Sender<StatusUpdate>, pub status_sender: broadcast::Sender<StatusUpdate>,
} }
#[derive(Debug)] #[derive(Debug, Error)]
pub enum ListenerError { pub enum ListenerError {
KafkaError(KafkaError), #[error("Kafka错误: {0}")]
JsonError(serde_json::Error), KafkaError(#[from] KafkaError),
#[error("JSON错误: {0}")]
JsonError(#[from] serde_json::Error),
#[error("消息错误: {0}")]
MessageError(String), 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 { impl KafkaListener {
pub fn new(config: &Config) -> Result<(Self, broadcast::Receiver<StatusUpdate>), KafkaError> { pub fn new(config: &Config) -> Result<(Self, broadcast::Receiver<StatusUpdate>), KafkaError> {
let client: StreamConsumer = ClientConfig::new() let client: StreamConsumer = ClientConfig::new()

View File

@ -10,7 +10,11 @@ mod services;
use app::create_router; use app::create_router;
use clap::Parser; 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 config::Config;
use db::{create_pool, run_migrations}; use db::{create_pool, run_migrations};
use listener::KafkaListener; use listener::KafkaListener;
@ -25,6 +29,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
match cli.command.unwrap_or_default() { match cli.command.unwrap_or_default() {
Commands::Serve(args) => serve_command(args).await, Commands::Serve(args) => serve_command(args).await,
Commands::Migrate(args) => migrate_command(args).await, Commands::Migrate(args) => migrate_command(args).await,
Commands::Permissions(args) => permissions_command(args).await,
Commands::Version => version_command(), Commands::Version => version_command(),
Commands::Config => config_command(), Commands::Config => config_command(),
} }
@ -128,7 +133,7 @@ async fn serve_command(args: ServeArgs) -> Result<(), Box<dyn std::error::Error>
}; };
println!("⚙️ 正在创建GraphQL路由..."); 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); let bind_addr = format!("{}:{}", args.host, config.port);
println!("🔌 正在绑定到地址: {}", bind_addr); println!("🔌 正在绑定到地址: {}", bind_addr);
@ -237,6 +242,230 @@ fn config_command() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) 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 { fn mask_database_url(url: &str) -> String {
if let Some(at_pos) = url.find('@') { if let Some(at_pos) = url.find('@') {
if let Some(scheme_end) = url.find("://") { if let Some(scheme_end) = url.find("://") {

View File

@ -1,9 +1,11 @@
pub mod invite_code; pub mod invite_code;
pub mod kafka_message; pub mod kafka_message;
pub mod page_block;
pub mod settings; pub mod settings;
pub mod user; pub mod user;
pub use invite_code::*; pub use invite_code::*;
pub use kafka_message::*; pub use kafka_message::*;
pub use page_block::*;
pub use settings::*; pub use settings::*;
pub use user::*; pub use user::*;

621
src/models/page_block.rs Normal file
View 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,
}

View 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),
}
}
}

View File

@ -1,5 +1,7 @@
pub mod casbin_service;
pub mod invite_code_service; pub mod invite_code_service;
pub mod mosaic_service; pub mod mosaic_service;
pub mod page_block_service;
pub mod query_builder; pub mod query_builder;
pub mod settings_manager; pub mod settings_manager;
pub mod settings_service; pub mod settings_service;

File diff suppressed because it is too large Load Diff

View File

@ -54,6 +54,11 @@ impl SettingsService {
} }
} }
/// 获取数据库连接池的引用(用于事务)
pub fn get_pool(&self) -> &PgPool {
&self.pool
}
/// 创建新的配置项 /// 创建新的配置项
pub async fn create_setting( pub async fn create_setting(
&self, &self,