mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 14:10:52 +01:00
refactore manifest and permission
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
//import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
@ -99,7 +99,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
//plugins: [tailwindcss()],
|
||||
// Better support for Tauri CLI output
|
||||
clearScreen: false,
|
||||
// Enable environment variables
|
||||
|
||||
479
src-tauri/Cargo.lock
generated
479
src-tauri/Cargo.lock
generated
@ -17,6 +17,17 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@ -62,6 +73,15 @@ version = "1.0.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.11.0"
|
||||
@ -274,6 +294,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
@ -386,6 +412,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff"
|
||||
dependencies = [
|
||||
"libbz2-rs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.18.5"
|
||||
@ -440,7 +475,7 @@ dependencies = [
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -459,6 +494,8 @@ version = "1.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
@ -514,6 +551,16 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@ -533,6 +580,18 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
@ -627,6 +686,21 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
@ -698,6 +772,33 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.10"
|
||||
@ -739,6 +840,22 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
@ -749,6 +866,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.19"
|
||||
@ -770,6 +898,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -890,6 +1019,30 @@ version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "3.0.2"
|
||||
@ -1020,6 +1173,12 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "field-offset"
|
||||
version = "0.3.6"
|
||||
@ -1032,11 +1191,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.0"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
|
||||
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"libz-rs-sys",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
@ -1081,9 +1241,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
@ -1533,6 +1693,7 @@ name = "haex-hub"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
"fs_extra",
|
||||
"hex",
|
||||
"mime",
|
||||
@ -1540,10 +1701,10 @@ dependencies = [
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sqlparser",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-android-fs",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-http",
|
||||
@ -1552,11 +1713,12 @@ dependencies = [
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-persisted-scope",
|
||||
"tauri-plugin-store",
|
||||
"thiserror 2.0.16",
|
||||
"tokio",
|
||||
"thiserror 2.0.17",
|
||||
"ts-rs",
|
||||
"uhlc",
|
||||
"url",
|
||||
"uuid",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1607,6 +1769,15 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.29.1"
|
||||
@ -1887,9 +2058,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.3"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
@ -1937,6 +2108,15 @@ dependencies = [
|
||||
"cfb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.8"
|
||||
@ -2024,6 +2204,16 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
@ -2109,6 +2299,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libbz2-rs-sys"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.175"
|
||||
@ -2147,6 +2343,15 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-rs-sys"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd"
|
||||
dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@ -2187,6 +2392,16 @@ version = "0.4.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rust2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a"
|
||||
dependencies = [
|
||||
"crc",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@ -2305,7 +2520,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"png",
|
||||
"serde",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@ -2775,10 +2990,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
@ -2937,6 +3162,16 @@ dependencies = [
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
@ -2990,6 +3225,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppmd-rust"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@ -3129,7 +3370,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.8",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@ -3149,7 +3390,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@ -3338,7 +3579,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"libredox",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3646,9 +3887,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.226"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
@ -3667,18 +3908,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.226"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.226"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3813,10 +4054,21 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
@ -3838,6 +4090,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
@ -3946,10 +4207,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
|
||||
[[package]]
|
||||
name = "sqlparser"
|
||||
version = "0.58.0"
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec4b661c54b1e4b603b37873a18c59920e4c51ea8ea2cf527d925424dbd4437c"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparser"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4591acadbcf52f0af60eafbb2c003232b2b4cd8de5f0e9437cb8b1b59046cc0f"
|
||||
dependencies = [
|
||||
"log",
|
||||
"recursive",
|
||||
@ -4224,7 +4495,7 @@ dependencies = [
|
||||
"tauri-runtime",
|
||||
"tauri-runtime-wry",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tray-icon",
|
||||
"url",
|
||||
@ -4277,7 +4548,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"syn 2.0.100",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
"url",
|
||||
"uuid",
|
||||
@ -4315,21 +4586,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-android-fs"
|
||||
version = "12.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1387b55109ae9b8ad0521ac11f8ce827740f53c0e0ce74648d1cb2efe0fd9c09"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.4.0"
|
||||
@ -4344,7 +4600,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -4365,7 +4621,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"toml 0.9.5",
|
||||
"url",
|
||||
]
|
||||
@ -4388,7 +4644,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"url",
|
||||
"urlpattern",
|
||||
@ -4408,7 +4664,7 @@ dependencies = [
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
@ -4429,7 +4685,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"url",
|
||||
"windows",
|
||||
"zbus",
|
||||
@ -4450,7 +4706,7 @@ dependencies = [
|
||||
"sys-locale",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4466,7 +4722,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4480,7 +4736,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@ -4503,7 +4759,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
@ -4567,7 +4823,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"swift-rs",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"toml 0.9.5",
|
||||
"url",
|
||||
"urlpattern",
|
||||
@ -4592,7 +4848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
|
||||
dependencies = [
|
||||
"quick-xml 0.37.5",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"windows",
|
||||
"windows-version",
|
||||
]
|
||||
@ -4641,11 +4897,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.16"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
|
||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.16",
|
||||
"thiserror-impl 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4661,9 +4917,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.16"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
|
||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -4951,7 +5207,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"png",
|
||||
"serde",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@ -4967,7 +5223,7 @@ version = "11.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be"
|
||||
dependencies = [
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"ts-rs-macros",
|
||||
]
|
||||
|
||||
@ -5087,9 +5343,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.4"
|
||||
version = "2.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
|
||||
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
@ -5405,7 +5661,7 @@ version = "0.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
|
||||
dependencies = [
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"windows",
|
||||
"windows-core 0.61.0",
|
||||
]
|
||||
@ -5965,7 +6221,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"soup3",
|
||||
"tao-macros",
|
||||
"thiserror 2.0.16",
|
||||
"thiserror 2.0.17",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
@ -6128,6 +6384,20 @@ name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
@ -6151,6 +6421,79 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "5.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arbitrary",
|
||||
"bzip2",
|
||||
"constant_time_eq",
|
||||
"crc32fast",
|
||||
"deflate64",
|
||||
"flate2",
|
||||
"getrandom 0.3.2",
|
||||
"hmac",
|
||||
"indexmap 2.8.0",
|
||||
"lzma-rust2",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"ppmd-rust",
|
||||
"sha1",
|
||||
"time",
|
||||
"zeroize",
|
||||
"zopfli",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.7.0"
|
||||
|
||||
@ -18,37 +18,38 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
serde_json = "1.0.145"
|
||||
|
||||
tauri-build = { version = "2.2", features = [] }
|
||||
serde = { version = "1.0.226", features = ["derive"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
[dependencies]
|
||||
rusqlite = { version = "0.37.0", features = [
|
||||
"load_extension",
|
||||
"bundled-sqlcipher-vendored-openssl",
|
||||
"functions",
|
||||
] }
|
||||
#libsqlite3-sys = { version = "0.31", features = ["bundled-sqlcipher"] }
|
||||
|
||||
#tauri-plugin-sql = { version = "2", features = ["sqlite"] }tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }#libsqlite3-sys = { version = "0.31", features = ["bundled-sqlcipher"] }
|
||||
#sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] }
|
||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
hex = "0.4"
|
||||
serde_json = "1.0.143"
|
||||
base64 = "0.22"
|
||||
mime_guess = "2.0"
|
||||
mime = "0.3"
|
||||
ed25519-dalek = "2.1"
|
||||
fs_extra = "1.3.0"
|
||||
sqlparser = { version = "0.58.0", features = ["visitor"] }
|
||||
uhlc = "0.8"
|
||||
hex = "0.4"
|
||||
mime = "0.3"
|
||||
mime_guess = "2.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.143"
|
||||
sha2 = "0.10.9"
|
||||
sqlparser = { version = "0.59.0", features = ["visitor"] }
|
||||
tauri = { version = "2.8.5", features = ["protocol-asset", "devtools"] }
|
||||
tauri-plugin-dialog = "2.4.0"
|
||||
tauri-plugin-fs = "2.4.0"
|
||||
tauri-plugin-opener = "2.5.0"
|
||||
tauri-plugin-os = "2.3"
|
||||
tauri-plugin-store = "2.4.0"
|
||||
tauri-plugin-http = "2.5.2"
|
||||
tauri-plugin-notification = "2.3.1"
|
||||
tauri-plugin-opener = "2.5.0"
|
||||
tauri-plugin-os = "2.3"
|
||||
tauri-plugin-persisted-scope = "2.3.2"
|
||||
tauri-plugin-android-fs = "12.0.1"
|
||||
uuid = { version = "1.18.1", features = ["v4"] }
|
||||
tauri-plugin-store = "2.4.0"
|
||||
thiserror = "2.0.17"
|
||||
ts-rs = "11.0.1"
|
||||
thiserror = "2.0.16"
|
||||
|
||||
#tauri-plugin-sql = { version = "2", features = ["sqlite"] }
|
||||
uhlc = "0.8"
|
||||
uuid = { version = "1.18.1", features = ["v4"] }
|
||||
zip = "5.1.1"
|
||||
url = "2.5.7"
|
||||
|
||||
@ -25,7 +25,6 @@
|
||||
"fs:allow-download-read-recursive",
|
||||
"fs:allow-download-write-recursive",
|
||||
"fs:default",
|
||||
"android-fs:default",
|
||||
{
|
||||
"identifier": "fs:scope",
|
||||
"allow": [{ "path": "**" }]
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { drizzle } from 'drizzle-orm/sqlite-proxy' // Adapter für Query Building ohne direkte Verbindung
|
||||
import * as schema from './schemas/vault' // Importiere alles aus deiner Schema-Datei
|
||||
import * as schema from './schemas/haex' // Importiere alles aus deiner Schema-Datei
|
||||
|
||||
// sqlite-proxy benötigt eine (dummy) Ausführungsfunktion als Argument.
|
||||
// Diese wird in unserem Tauri-Workflow nie aufgerufen, da wir nur .toSQL() verwenden.
|
||||
|
||||
15
src-tauri/database/migrations/0012_special_gwen_stacy.sql
Normal file
15
src-tauri/database/migrations/0012_special_gwen_stacy.sql
Normal file
@ -0,0 +1,15 @@
|
||||
ALTER TABLE `haex_extension_permissions` RENAME COLUMN "resource" TO "resource_type";--> statement-breakpoint
|
||||
ALTER TABLE `haex_extension_permissions` RENAME COLUMN "operation" TO "action";--> statement-breakpoint
|
||||
ALTER TABLE `haex_extension_permissions` RENAME COLUMN "path" TO "target";--> statement-breakpoint
|
||||
DROP INDEX `haex_extension_permissions_extension_id_resource_operation_path_unique`;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extension_permissions` ADD `constraints` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extension_permissions` ADD `status` text DEFAULT 'denied' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extension_permissions` ADD `haex_timestamp` text;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `haex_extension_permissions_extension_id_resource_type_action_target_unique` ON `haex_extension_permissions` (`extension_id`,`resource_type`,`action`,`target`);--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions` ADD `description` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions` ADD `entry` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions` ADD `homepage` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions` ADD `public_key` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions` ADD `signature` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions` ADD `haex_timestamp` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_settings` ADD `haex_timestamp` text;
|
||||
900
src-tauri/database/migrations/meta/0012_snapshot.json
Normal file
900
src-tauri/database/migrations/meta/0012_snapshot.json
Normal file
@ -0,0 +1,900 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "e4f45178-afcf-46e9-9a07-79b7bca88d88",
|
||||
"prevId": "c8c0825d-c435-4a42-986a-a4f70e7f9e8b",
|
||||
"tables": {
|
||||
"haex_crdt_configs": {
|
||||
"name": "haex_crdt_configs",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_crdt_logs": {
|
||||
"name": "haex_crdt_logs",
|
||||
"columns": {
|
||||
"hlc_timestamp": {
|
||||
"name": "hlc_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"table_name": {
|
||||
"name": "table_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"row_pks": {
|
||||
"name": "row_pks",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"op_type": {
|
||||
"name": "op_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"column_name": {
|
||||
"name": "column_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"new_value": {
|
||||
"name": "new_value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"old_value": {
|
||||
"name": "old_value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_crdt_snapshots": {
|
||||
"name": "haex_crdt_snapshots",
|
||||
"columns": {
|
||||
"snapshot_id": {
|
||||
"name": "snapshot_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created": {
|
||||
"name": "created",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"epoch_hlc": {
|
||||
"name": "epoch_hlc",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"location_url": {
|
||||
"name": "location_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"file_size_bytes": {
|
||||
"name": "file_size_bytes",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extension_permissions": {
|
||||
"name": "haex_extension_permissions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"extension_id": {
|
||||
"name": "extension_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"resource_type": {
|
||||
"name": "resource_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"action": {
|
||||
"name": "action",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"target": {
|
||||
"name": "target",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"constraints": {
|
||||
"name": "constraints",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'denied'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"haex_tombstone": {
|
||||
"name": "haex_tombstone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"haex_timestamp": {
|
||||
"name": "haex_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_extension_permissions_extension_id_resource_type_action_target_unique": {
|
||||
"name": "haex_extension_permissions_extension_id_resource_type_action_target_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource_type",
|
||||
"action",
|
||||
"target"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extension_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extension_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extension_permissions",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions": {
|
||||
"name": "haex_extensions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"author": {
|
||||
"name": "author",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"entry": {
|
||||
"name": "entry",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"homepage": {
|
||||
"name": "homepage",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"icon": {
|
||||
"name": "icon",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"public_key": {
|
||||
"name": "public_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"signature": {
|
||||
"name": "signature",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"haex_tombstone": {
|
||||
"name": "haex_tombstone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"haex_timestamp": {
|
||||
"name": "haex_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_settings": {
|
||||
"name": "haex_settings",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"haex_tombstone": {
|
||||
"name": "haex_tombstone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"haex_timestamp": {
|
||||
"name": "haex_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_notifications": {
|
||||
"name": "haex_notifications",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"alt": {
|
||||
"name": "alt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"date": {
|
||||
"name": "date",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"icon": {
|
||||
"name": "icon",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"read": {
|
||||
"name": "read",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"source": {
|
||||
"name": "source",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"text": {
|
||||
"name": "text",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"haex_tombstone": {
|
||||
"name": "haex_tombstone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_passwords_group_items": {
|
||||
"name": "haex_passwords_group_items",
|
||||
"columns": {
|
||||
"group_id": {
|
||||
"name": "group_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"item_id": {
|
||||
"name": "item_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"haex_tombstone": {
|
||||
"name": "haex_tombstone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"haex_passwords_group_items_group_id_haex_passwords_groups_id_fk": {
|
||||
"name": "haex_passwords_group_items_group_id_haex_passwords_groups_id_fk",
|
||||
"tableFrom": "haex_passwords_group_items",
|
||||
"tableTo": "haex_passwords_groups",
|
||||
"columnsFrom": [
|
||||
"group_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk": {
|
||||
"name": "haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk",
|
||||
"tableFrom": "haex_passwords_group_items",
|
||||
"tableTo": "haex_passwords_item_details",
|
||||
"columnsFrom": [
|
||||
"item_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"haex_passwords_group_items_item_id_group_id_pk": {
|
||||
"columns": [
|
||||
"item_id",
|
||||
"group_id"
|
||||
],
|
||||
"name": "haex_passwords_group_items_item_id_group_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_passwords_groups": {
|
||||
"name": "haex_passwords_groups",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"icon": {
|
||||
"name": "icon",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"order": {
|
||||
"name": "order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"parent_id": {
|
||||
"name": "parent_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"haex_tombstone": {
|
||||
"name": "haex_tombstone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"haex_passwords_groups_parent_id_haex_passwords_groups_id_fk": {
|
||||
"name": "haex_passwords_groups_parent_id_haex_passwords_groups_id_fk",
|
||||
"tableFrom": "haex_passwords_groups",
|
||||
"tableTo": "haex_passwords_groups",
|
||||
"columnsFrom": [
|
||||
"parent_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_passwords_item_details": {
|
||||
"name": "haex_passwords_item_details",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"note": {
|
||||
"name": "note",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"icon": {
|
||||
"name": "icon",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"tags": {
|
||||
"name": "tags",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"haex_tombstone": {
|
||||
"name": "haex_tombstone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_passwords_item_history": {
|
||||
"name": "haex_passwords_item_history",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"item_id": {
|
||||
"name": "item_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"changed_property": {
|
||||
"name": "changed_property",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"old_value": {
|
||||
"name": "old_value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"new_value": {
|
||||
"name": "new_value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"haex_tombstone": {
|
||||
"name": "haex_tombstone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk": {
|
||||
"name": "haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk",
|
||||
"tableFrom": "haex_passwords_item_history",
|
||||
"tableTo": "haex_passwords_item_details",
|
||||
"columnsFrom": [
|
||||
"item_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_passwords_item_key_values": {
|
||||
"name": "haex_passwords_item_key_values",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"item_id": {
|
||||
"name": "item_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"haex_tombstone": {
|
||||
"name": "haex_tombstone",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk": {
|
||||
"name": "haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk",
|
||||
"tableFrom": "haex_passwords_item_key_values",
|
||||
"tableTo": "haex_passwords_item_details",
|
||||
"columnsFrom": [
|
||||
"item_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {
|
||||
"\"haex_extension_permissions\".\"resource\"": "\"haex_extension_permissions\".\"resource_type\"",
|
||||
"\"haex_extension_permissions\".\"operation\"": "\"haex_extension_permissions\".\"action\"",
|
||||
"\"haex_extension_permissions\".\"path\"": "\"haex_extension_permissions\".\"target\""
|
||||
}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -85,6 +85,13 @@
|
||||
"when": 1757968140525,
|
||||
"tag": "0011_illegal_thor_girl",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "6",
|
||||
"when": 1759362109283,
|
||||
"tag": "0012_special_gwen_stacy",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
76
src-tauri/database/schemas/haex.ts
Normal file
76
src-tauri/database/schemas/haex.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import {
|
||||
integer,
|
||||
sqliteTable,
|
||||
text,
|
||||
unique,
|
||||
type AnySQLiteColumn,
|
||||
} from 'drizzle-orm/sqlite-core'
|
||||
import tableNames from '../tableNames.json'
|
||||
|
||||
export const haexSettings = sqliteTable(tableNames.haex.settings, {
|
||||
id: text().primaryKey(),
|
||||
key: text(),
|
||||
type: text(),
|
||||
value: text(),
|
||||
haex_tombstone: integer({ mode: 'boolean' }),
|
||||
haex_timestamp: text(),
|
||||
})
|
||||
export type InsertHaexSettings = typeof haexSettings.$inferInsert
|
||||
export type SelectHaexSettings = typeof haexSettings.$inferSelect
|
||||
|
||||
export const haexExtensions = sqliteTable(tableNames.haex.extensions, {
|
||||
id: text().primaryKey(),
|
||||
author: text(),
|
||||
description: text(),
|
||||
entry: text(),
|
||||
homepage: text(),
|
||||
enabled: integer({ mode: 'boolean' }),
|
||||
icon: text(),
|
||||
name: text(),
|
||||
public_key: text(),
|
||||
signature: text(),
|
||||
url: text(),
|
||||
version: text(),
|
||||
haex_tombstone: integer({ mode: 'boolean' }),
|
||||
haex_timestamp: text(),
|
||||
})
|
||||
export type InsertHaexExtensions = typeof haexExtensions.$inferInsert
|
||||
export type SelectHaexExtensions = typeof haexExtensions.$inferSelect
|
||||
|
||||
export const haexExtensionPermissions = sqliteTable(
|
||||
tableNames.haex.extension_permissions,
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
extensionId: text('extension_id').references(
|
||||
(): AnySQLiteColumn => haexExtensions.id,
|
||||
),
|
||||
resourceType: text('resource_type', {
|
||||
enum: ['fs', 'http', 'db', 'shell'],
|
||||
}),
|
||||
action: text({ enum: ['read', 'write'] }),
|
||||
target: text(),
|
||||
constraints: text({ mode: 'json' }),
|
||||
status: text({ enum: ['ask', 'granted', 'denied'] })
|
||||
.notNull()
|
||||
.default('denied'),
|
||||
createdAt: text('created_at').default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
||||
() => new Date(),
|
||||
),
|
||||
haexTombstone: integer('haex_tombstone', { mode: 'boolean' }),
|
||||
haexTimestamp: text('haex_timestamp'),
|
||||
},
|
||||
(table) => [
|
||||
unique().on(
|
||||
table.extensionId,
|
||||
table.resourceType,
|
||||
table.action,
|
||||
table.target,
|
||||
),
|
||||
],
|
||||
)
|
||||
export type InserthaexExtensionPermissions =
|
||||
typeof haexExtensionPermissions.$inferInsert
|
||||
export type SelecthaexExtensionPermissions =
|
||||
typeof haexExtensionPermissions.$inferSelect
|
||||
@ -4,59 +4,10 @@ import {
|
||||
primaryKey,
|
||||
sqliteTable,
|
||||
text,
|
||||
unique,
|
||||
type AnySQLiteColumn,
|
||||
} from 'drizzle-orm/sqlite-core'
|
||||
import tableNames from '../tableNames.json'
|
||||
|
||||
export const haexSettings = sqliteTable(tableNames.haex.settings, {
|
||||
id: text().primaryKey(),
|
||||
key: text(),
|
||||
type: text(),
|
||||
value: text(),
|
||||
haex_tombstone: integer({ mode: 'boolean' }),
|
||||
})
|
||||
export type InsertHaexSettings = typeof haexSettings.$inferInsert
|
||||
export type SelectHaexSettings = typeof haexSettings.$inferSelect
|
||||
|
||||
export const haexExtensions = sqliteTable(tableNames.haex.extensions, {
|
||||
id: text().primaryKey(),
|
||||
author: text(),
|
||||
enabled: integer({ mode: 'boolean' }),
|
||||
icon: text(),
|
||||
name: text(),
|
||||
url: text(),
|
||||
version: text(),
|
||||
haex_tombstone: integer({ mode: 'boolean' }),
|
||||
})
|
||||
export type InsertHaexExtensions = typeof haexExtensions.$inferInsert
|
||||
export type SelectHaexExtensions = typeof haexExtensions.$inferSelect
|
||||
|
||||
export const haexExtensionPermissions = sqliteTable(
|
||||
tableNames.haex.extension_permissions,
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
extensionId: text('extension_id').references(
|
||||
(): AnySQLiteColumn => haexExtensions.id,
|
||||
),
|
||||
resource: text({ enum: ['fs', 'http', 'db', 'shell'] }),
|
||||
operation: text({ enum: ['read', 'write', 'create'] }),
|
||||
path: text(),
|
||||
createdAt: text('created_at').default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
||||
() => new Date(),
|
||||
),
|
||||
haex_tombstone: integer({ mode: 'boolean' }),
|
||||
},
|
||||
(table) => [
|
||||
unique().on(table.extensionId, table.resource, table.operation, table.path),
|
||||
],
|
||||
)
|
||||
export type InserthaexExtensionPermissions =
|
||||
typeof haexExtensionPermissions.$inferInsert
|
||||
export type SelecthaexExtensionPermissions =
|
||||
typeof haexExtensionPermissions.$inferSelect
|
||||
|
||||
export const haexNotifications = sqliteTable(tableNames.haex.notifications, {
|
||||
id: text().primaryKey(),
|
||||
alt: text(),
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-webview-show","core:webview:default","core:window:allow-create","core:window:allow-get-all-windows","core:window:allow-show","core:window:default","dialog:default","fs:allow-appconfig-read-recursive","fs:allow-appconfig-write-recursive","fs:allow-appdata-read-recursive","fs:allow-appdata-write-recursive","fs:allow-read-file","fs:allow-read-dir","fs:allow-resource-read-recursive","fs:allow-resource-write-recursive","fs:allow-download-read-recursive","fs:allow-download-write-recursive","fs:default","android-fs:default",{"identifier":"fs:scope","allow":[{"path":"**"}]},"http:allow-fetch-send","http:allow-fetch","http:default","notification:allow-create-channel","notification:allow-list-channels","notification:allow-notify","notification:default","opener:allow-open-url","opener:default","os:allow-hostname","os:default","store:default"]}}
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-webview-show","core:webview:default","core:window:allow-create","core:window:allow-get-all-windows","core:window:allow-show","core:window:default","dialog:default","fs:allow-appconfig-read-recursive","fs:allow-appconfig-write-recursive","fs:allow-appdata-read-recursive","fs:allow-appdata-write-recursive","fs:allow-read-file","fs:allow-read-dir","fs:allow-resource-read-recursive","fs:allow-resource-write-recursive","fs:allow-download-read-recursive","fs:allow-download-write-recursive","fs:default",{"identifier":"fs:scope","allow":[{"path":"**"}]},"http:allow-fetch-send","http:allow-fetch","http:default","notification:allow-create-channel","notification:allow-list-channels","notification:allow-notify","notification:default","opener:allow-open-url","opener:default","os:allow-hostname","os:default","store:default"]}}
|
||||
@ -2270,12 +2270,6 @@
|
||||
"Identifier": {
|
||||
"description": "Permission identifier",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Default permissions for the plugin",
|
||||
"type": "string",
|
||||
"const": "android-fs:default",
|
||||
"markdownDescription": "Default permissions for the plugin"
|
||||
},
|
||||
{
|
||||
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
|
||||
"type": "string",
|
||||
|
||||
@ -2270,12 +2270,6 @@
|
||||
"Identifier": {
|
||||
"description": "Permission identifier",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Default permissions for the plugin",
|
||||
"type": "string",
|
||||
"const": "android-fs:default",
|
||||
"markdownDescription": "Default permissions for the plugin"
|
||||
},
|
||||
{
|
||||
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
|
||||
"type": "string",
|
||||
|
||||
@ -10,8 +10,8 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::{AppHandle, Wry};
|
||||
use tauri_plugin_store::{Store, StoreExt};
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_store::StoreExt;
|
||||
use thiserror::Error;
|
||||
use uhlc::{HLCBuilder, Timestamp, HLC, ID};
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -755,6 +755,7 @@ impl CrdtTransformer {
|
||||
selection: del_stmt.selection.clone(),
|
||||
returning: None,
|
||||
or: None,
|
||||
limit: None,
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// src-tauri/src/crdt/trigger.rs
|
||||
use crate::table_names::TABLE_CRDT_LOGS;
|
||||
use rusqlite::{Connection, Result as RusqliteResult, Row, Transaction};
|
||||
use serde::Serialize;
|
||||
@ -11,7 +12,7 @@ const UPDATE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_update";
|
||||
|
||||
//const SYNC_ACTIVE_KEY: &str = "sync_active";
|
||||
pub const TOMBSTONE_COLUMN: &str = "haex_tombstone";
|
||||
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_hlc_timestamp";
|
||||
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_timestamp";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CrdtSetupError {
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
// src-tauri/src/database/core.rs
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::database::DbConnection;
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
@ -14,6 +12,7 @@ use serde_json::Value as JsonValue;
|
||||
use sqlparser::ast::{Query, Select, SetExpr, Statement, TableFactor, TableObject};
|
||||
use sqlparser::dialect::SQLiteDialect;
|
||||
use sqlparser::parser::Parser;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Öffnet und initialisiert eine Datenbank mit Verschlüsselung
|
||||
pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connection, DatabaseError> {
|
||||
@ -376,6 +375,7 @@ fn extract_tables_from_set_expr_recursive(set_expr: &SetExpr, tables: &mut Vec<S
|
||||
| SetExpr::Table(_)
|
||||
| SetExpr::Insert(_)
|
||||
| SetExpr::Update(_)
|
||||
| SetExpr::Merge(_)
|
||||
| SetExpr::Delete(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,528 +0,0 @@
|
||||
/// src-tauri/src/extension/core.rs
|
||||
use crate::extension::database::permissions::DbExtensionPermission;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permission_manager::ExtensionPermissions;
|
||||
use mime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tauri::{
|
||||
http::{Request, Response},
|
||||
AppHandle, Error as TauriError, Manager, Runtime, UriSchemeContext,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionManifest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub entry: String,
|
||||
pub icon: Option<String>,
|
||||
pub permissions: ExtensionPermissions,
|
||||
pub homepage: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionInfoResponse {
|
||||
pub key_hash: String,
|
||||
pub name: String,
|
||||
pub full_id: String,
|
||||
pub version: String,
|
||||
pub display_name: Option<String>,
|
||||
pub namespace: Option<String>,
|
||||
pub allowed_origin: String,
|
||||
}
|
||||
|
||||
impl ExtensionInfoResponse {
|
||||
pub fn from_extension(extension: &Extension) -> Self {
|
||||
// Bestimme die allowed_origin basierend auf Tauri-Konfiguration
|
||||
let allowed_origin = get_tauri_origin();
|
||||
|
||||
Self {
|
||||
key_hash: calculate_key_hash(&extension.manifest.id),
|
||||
name: extension.manifest.name.clone(),
|
||||
full_id: format!(
|
||||
"{}/{}@{}",
|
||||
calculate_key_hash(&extension.manifest.id),
|
||||
extension.manifest.name,
|
||||
extension.manifest.version
|
||||
),
|
||||
version: extension.manifest.version.clone(),
|
||||
display_name: Some(extension.manifest.name.clone()),
|
||||
namespace: extension.manifest.author.clone(),
|
||||
allowed_origin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tauri_origin() -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
"https://tauri.localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// Dummy-Funktion für Key Hash (du implementierst das richtig mit SHA-256)
|
||||
fn calculate_key_hash(id: &str) -> String {
|
||||
// TODO: Implementiere SHA-256 Hash vom Public Key
|
||||
// Für jetzt nur Placeholder
|
||||
format!("{:0<20}", id.chars().take(20).collect::<String>())
|
||||
}
|
||||
/// Extension source type (production vs development)
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExtensionSource {
|
||||
Production {
|
||||
path: PathBuf,
|
||||
version: String,
|
||||
},
|
||||
Development {
|
||||
dev_server_url: String,
|
||||
manifest_path: PathBuf,
|
||||
auto_reload: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Complete extension data structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Extension {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub source: ExtensionSource,
|
||||
pub manifest: ExtensionManifest,
|
||||
pub enabled: bool,
|
||||
pub last_accessed: SystemTime,
|
||||
}
|
||||
|
||||
/// Cached permission data for performance
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedPermission {
|
||||
pub permissions: Vec<DbExtensionPermission>,
|
||||
pub cached_at: SystemTime,
|
||||
pub ttl: Duration,
|
||||
}
|
||||
|
||||
/// Enhanced extension manager
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionManager {
|
||||
pub production_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub dev_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
|
||||
}
|
||||
|
||||
impl ExtensionManager {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Validate filesystem permissions
|
||||
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
|
||||
fs_perms.validate()?;
|
||||
}
|
||||
*/
|
||||
match &extension.source {
|
||||
ExtensionSource::Production { .. } => {
|
||||
let mut extensions = self.production_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Production source".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_dev_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Validate filesystem permissions
|
||||
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
|
||||
fs_perms.validate()?;
|
||||
} */
|
||||
|
||||
match &extension.source {
|
||||
ExtensionSource::Development { .. } => {
|
||||
let mut extensions = self.dev_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Development source".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, extension_id: &str) -> Option<Extension> {
|
||||
// Dev extensions take priority
|
||||
let dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if let Some(extension) = dev_extensions.get(extension_id) {
|
||||
return Some(extension.clone());
|
||||
}
|
||||
|
||||
// Then check production
|
||||
let prod_extensions = self.production_extensions.lock().unwrap();
|
||||
prod_extensions.get(extension_id).cloned()
|
||||
}
|
||||
|
||||
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
|
||||
{
|
||||
let mut dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if dev_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut prod_extensions = self.production_extensions.lock().unwrap();
|
||||
if prod_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ExtensionError::NotFound {
|
||||
id: extension_id.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// For backward compatibility
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionState {
|
||||
pub extensions: Mutex<HashMap<String, ExtensionManifest>>,
|
||||
}
|
||||
|
||||
impl ExtensionState {
|
||||
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
|
||||
let mut extensions = self.extensions.lock().unwrap();
|
||||
extensions.insert(path, manifest);
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
|
||||
let extensions = self.extensions.lock().unwrap();
|
||||
extensions.values().find(|p| p.name == addon_id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ExtensionInfo {
|
||||
id: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DataProcessingError {
|
||||
HexDecoding(hex::FromHexError),
|
||||
Utf8Conversion(std::string::FromUtf8Error),
|
||||
JsonParsing(serde_json::Error),
|
||||
}
|
||||
|
||||
// Implementierung von Display für benutzerfreundliche Fehlermeldungen
|
||||
impl fmt::Display for DataProcessingError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DataProcessingError::HexDecoding(e) => write!(f, "Hex-Dekodierungsfehler: {}", e),
|
||||
DataProcessingError::Utf8Conversion(e) => {
|
||||
write!(f, "UTF-8-Konvertierungsfehler: {}", e)
|
||||
}
|
||||
DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implementierung von std::error::Error (optional, aber gute Praxis für bibliotheksähnlichen Code)
|
||||
impl std::error::Error for DataProcessingError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
DataProcessingError::HexDecoding(e) => Some(e),
|
||||
DataProcessingError::Utf8Conversion(e) => Some(e),
|
||||
DataProcessingError::JsonParsing(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implementierung von From-Traits für einfache Verwendung des '?'-Operators
|
||||
impl From<hex::FromHexError> for DataProcessingError {
|
||||
fn from(err: hex::FromHexError) -> Self {
|
||||
DataProcessingError::HexDecoding(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for DataProcessingError {
|
||||
fn from(err: std::string::FromUtf8Error) -> Self {
|
||||
DataProcessingError::Utf8Conversion(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for DataProcessingError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
DataProcessingError::JsonParsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_directory(source: String, destination: String) -> Result<(), String> {
|
||||
println!(
|
||||
"Kopiere Verzeichnis von '{}' nach '{}'",
|
||||
source, destination
|
||||
);
|
||||
|
||||
let source_path = PathBuf::from(&source);
|
||||
let destination_path = PathBuf::from(&destination);
|
||||
|
||||
if !source_path.exists() || !source_path.is_dir() {
|
||||
return Err(format!(
|
||||
"Quellverzeichnis '{}' nicht gefunden oder ist kein Verzeichnis.",
|
||||
source
|
||||
));
|
||||
}
|
||||
|
||||
// Optionen für fs_extra::dir::copy
|
||||
let mut options = fs_extra::dir::CopyOptions::new();
|
||||
options.overwrite = true; // Überschreibe Zieldateien, falls sie existieren
|
||||
options.copy_inside = true; // Kopiere den *Inhalt* des Quellordners in den Zielordner
|
||||
// options.content_only = true; // Alternative: nur Inhalt kopieren, Zielordner muss existieren
|
||||
options.buffer_size = 64000; // Standard-Puffergröße, kann angepasst werden
|
||||
|
||||
// Führe die Kopieroperation aus
|
||||
match fs_extra::dir::copy(&source_path, &destination_path, &options) {
|
||||
Ok(bytes_copied) => {
|
||||
println!("Verzeichnis erfolgreich kopiert ({} bytes)", bytes_copied);
|
||||
Ok(()) // Erfolg signalisieren
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fehler beim Kopieren des Verzeichnisses: {}", e);
|
||||
Err(format!("Fehler beim Kopieren: {}", e.to_string())) // Fehler als String zurückgeben
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_secure_extension_asset_path<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
extension_id: &str,
|
||||
extension_version: &str,
|
||||
requested_asset_path: &str,
|
||||
) -> Result<PathBuf, String> {
|
||||
// 1. Validiere die Extension ID
|
||||
if extension_id.is_empty()
|
||||
|| !extension_id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-')
|
||||
{
|
||||
return Err(format!("Ungültige Extension ID: {}", extension_id));
|
||||
}
|
||||
|
||||
if extension_version.is_empty()
|
||||
|| !extension_version
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
|
||||
{
|
||||
return Err(format!(
|
||||
"Ungültige Extension Version: {}",
|
||||
extension_version
|
||||
));
|
||||
}
|
||||
|
||||
// 2. Bestimme das Basisverzeichnis für alle Erweiterungen (Resource Directory)
|
||||
let base_extensions_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir() // Korrekt für Ressourcen
|
||||
// Wenn du stattdessen App Local Data willst: .app_local_data_dir()
|
||||
.map_err(|e: TauriError| format!("Basis-Verzeichnis nicht gefunden: {}", e))?
|
||||
.join("extensions");
|
||||
|
||||
// 3. Verzeichnis für die spezifische Erweiterung
|
||||
let specific_extension_dir =
|
||||
base_extensions_dir.join(format!("{}/{}", extension_id, extension_version));
|
||||
|
||||
// 4. Bereinige den angeforderten Asset-Pfad
|
||||
let clean_relative_path = requested_asset_path
|
||||
.replace('\\', "/")
|
||||
.trim_start_matches('/')
|
||||
.split('/')
|
||||
.filter(|&part| !part.is_empty() && part != "." && part != "..")
|
||||
.collect::<PathBuf>();
|
||||
|
||||
if clean_relative_path.as_os_str().is_empty() && requested_asset_path != "/" {
|
||||
return Err("Leerer oder ungültiger Asset-Pfad".to_string());
|
||||
}
|
||||
|
||||
// 5. Setze den finalen Pfad zusammen
|
||||
let final_path = specific_extension_dir.join(clean_relative_path);
|
||||
|
||||
// 6. SICHERHEITSCHECK (wie vorher)
|
||||
match final_path.canonicalize() {
|
||||
Ok(canonical_path) => {
|
||||
let canonical_base = specific_extension_dir.canonicalize().map_err(|e| {
|
||||
format!(
|
||||
"Kann Basis-Pfad '{}' nicht kanonisieren: {}",
|
||||
specific_extension_dir.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
if canonical_path.starts_with(&canonical_base) {
|
||||
Ok(canonical_path)
|
||||
} else {
|
||||
eprintln!( /* ... Sicherheitswarnung ... */ );
|
||||
Err("Ungültiger oder nicht erlaubter Asset-Pfad (kanonisch)".to_string())
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Fehler bei canonicalize (z.B. Pfad existiert nicht)
|
||||
if final_path.starts_with(&specific_extension_dir) {
|
||||
Ok(final_path) // Nicht-kanonisierten Pfad zurückgeben
|
||||
} else {
|
||||
eprintln!( /* ... Sicherheitswarnung ... */ );
|
||||
Err("Ungültiger oder nicht erlaubter Asset-Pfad (nicht kanonisiert)".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extension_protocol_handler<R: Runtime>(
|
||||
context: &UriSchemeContext<'_, R>,
|
||||
request: &Request<Vec<u8>>,
|
||||
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
let uri_ref = request.uri();
|
||||
println!("Protokoll Handler für: {}", uri_ref);
|
||||
|
||||
let host = uri_ref
|
||||
.host()
|
||||
.ok_or("Kein Host (Extension ID) in URI gefunden")?
|
||||
.to_string();
|
||||
|
||||
let path_str = uri_ref.path();
|
||||
let segments_iter = path_str.split('/').filter(|s| !s.is_empty());
|
||||
let resource_segments: Vec<&str> = segments_iter.collect();
|
||||
let raw_asset_path = resource_segments.join("/");
|
||||
let asset_to_load = if raw_asset_path.is_empty() {
|
||||
"index.html"
|
||||
} else {
|
||||
&raw_asset_path
|
||||
};
|
||||
|
||||
match process_hex_encoded_json(&host) {
|
||||
Ok(info) => {
|
||||
println!("Daten erfolgreich verarbeitet:");
|
||||
println!(" ID: {}", info.id);
|
||||
println!(" Version: {}", info.version);
|
||||
let absolute_secure_path = resolve_secure_extension_asset_path(
|
||||
context.app_handle(),
|
||||
&info.id,
|
||||
&info.version,
|
||||
&asset_to_load,
|
||||
)?;
|
||||
|
||||
println!("absolute_secure_path: {}", absolute_secure_path.display());
|
||||
|
||||
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
|
||||
match fs::read(&absolute_secure_path) {
|
||||
Ok(content) => {
|
||||
let mime_type = mime_guess::from_path(&absolute_secure_path)
|
||||
.first_or(mime::APPLICATION_OCTET_STREAM)
|
||||
.to_string();
|
||||
let content_length = content.len();
|
||||
println!(
|
||||
"Liefere {} ({}, {} bytes) ", // Content-Length zum Log hinzugefügt
|
||||
absolute_secure_path.display(),
|
||||
mime_type,
|
||||
content_length
|
||||
);
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", mime_type)
|
||||
.header("Content-Length", content_length.to_string()) // <-- HIER HINZUGEFÜGT
|
||||
// Optional, aber gut für Streaming-Fähigkeit:
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.body(content)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Fehler beim Lesen der Datei {}: {}",
|
||||
absolute_secure_path.display(),
|
||||
e
|
||||
);
|
||||
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
|
||||
404
|
||||
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
403
|
||||
} else {
|
||||
500
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.status(status_code)
|
||||
.body(Vec::new()) // Leerer Body für Fehler
|
||||
.map_err(|e| e.into()) // Wandle http::Error in Box<dyn Error> um
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Datei nicht gefunden oder es ist keine Datei
|
||||
eprintln!(
|
||||
"Asset nicht gefunden oder ist kein File: {}",
|
||||
absolute_secure_path.display()
|
||||
);
|
||||
Response::builder()
|
||||
.status(404) // HTTP 404 Not Found
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fehler bei der Datenverarbeitung: {}", e);
|
||||
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.body(Vec::new()) // Leerer Body für Fehler
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_hex_encoded_json(hex_input: &str) -> Result<ExtensionInfo, DataProcessingError> {
|
||||
// Schritt 1: Hex-String zu Bytes dekodieren
|
||||
let bytes = hex::decode(hex_input)?; // Konvertiert hex::FromHexError automatisch
|
||||
|
||||
// Schritt 2: Bytes zu UTF-8-String konvertieren
|
||||
let json_string = String::from_utf8(bytes)?; // Konvertiert FromUtf8Error automatisch
|
||||
|
||||
// Schritt 3: JSON-String zu Struktur parsen
|
||||
let extension_info: ExtensionInfo = serde_json::from_str(&json_string)?; // Konvertiert serde_json::Error automatisch
|
||||
|
||||
Ok(extension_info)
|
||||
}
|
||||
311
src-tauri/src/extension/core/manager.rs
Normal file
311
src-tauri/src/extension/core/manager.rs
Normal file
@ -0,0 +1,311 @@
|
||||
// src-tauri/src/extension/core/manager.rs
|
||||
|
||||
use crate::extension::core::manifest::{EditablePermissions, ExtensionManifest, ExtensionPreview};
|
||||
use crate::extension::core::types::{copy_directory, Extension, ExtensionSource};
|
||||
use crate::extension::crypto::ExtensionCrypto;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::manager::PermissionManager;
|
||||
use crate::extension::permissions::types::{ExtensionPermission, PermissionStatus};
|
||||
use crate::AppState;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use zip::ZipArchive;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedPermission {
|
||||
pub permissions: Vec<ExtensionPermission>,
|
||||
pub cached_at: SystemTime,
|
||||
pub ttl: Duration,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionManager {
|
||||
pub production_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub dev_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
|
||||
}
|
||||
|
||||
impl ExtensionManager {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn get_base_extension_dir(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<PathBuf, ExtensionError> {
|
||||
let path = app_handle
|
||||
.path()
|
||||
.app_local_data_dir()
|
||||
.map_err(|e| ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
|
||||
})?
|
||||
.join("extensions");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn get_extension_dir(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
extension_id: &str,
|
||||
extension_version: &str,
|
||||
) -> Result<PathBuf, ExtensionError> {
|
||||
let specific_extension_dir = self
|
||||
.get_base_extension_dir(app_handle)?
|
||||
.join(extension_id)
|
||||
.join(extension_version);
|
||||
|
||||
Ok(specific_extension_dir)
|
||||
}
|
||||
|
||||
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
match &extension.source {
|
||||
ExtensionSource::Production { .. } => {
|
||||
let mut extensions = self.production_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Production source".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_dev_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
match &extension.source {
|
||||
ExtensionSource::Development { .. } => {
|
||||
let mut extensions = self.dev_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Development source".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, extension_id: &str) -> Option<Extension> {
|
||||
let dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if let Some(extension) = dev_extensions.get(extension_id) {
|
||||
return Some(extension.clone());
|
||||
}
|
||||
|
||||
let prod_extensions = self.production_extensions.lock().unwrap();
|
||||
prod_extensions.get(extension_id).cloned()
|
||||
}
|
||||
|
||||
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
|
||||
{
|
||||
let mut dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if dev_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut prod_extensions = self.production_extensions.lock().unwrap();
|
||||
if prod_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ExtensionError::NotFound {
|
||||
id: extension_id.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn remove_extension_internal(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
extension_id: String,
|
||||
extension_version: String,
|
||||
state: &State<'_, AppState>,
|
||||
) -> Result<(), ExtensionError> {
|
||||
PermissionManager::delete_permissions(state, &extension_id).await?;
|
||||
self.remove_extension(&extension_id)?;
|
||||
|
||||
let extension_dir =
|
||||
self.get_extension_dir(app_handle, &extension_id, &extension_version)?;
|
||||
|
||||
if extension_dir.exists() {
|
||||
std::fs::remove_dir_all(&extension_dir)
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn preview_extension_internal(
|
||||
&self,
|
||||
source_path: String,
|
||||
) -> Result<ExtensionPreview, ExtensionError> {
|
||||
let source = PathBuf::from(&source_path);
|
||||
|
||||
let temp = std::env::temp_dir().join(format!("haexhub_preview_{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
let mut archive =
|
||||
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Invalid ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
archive
|
||||
.extract(&temp)
|
||||
.map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Cannot extract ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest_path = temp.join("manifest.json");
|
||||
let manifest_content =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read manifest: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
let content_hash = ExtensionCrypto::hash_directory(&temp)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
|
||||
let is_valid_signature = ExtensionCrypto::verify_signature(
|
||||
&manifest.public_key,
|
||||
&content_hash,
|
||||
&manifest.signature,
|
||||
)
|
||||
.is_ok();
|
||||
|
||||
let key_hash = manifest.calculate_key_hash()?;
|
||||
let editable_permissions = manifest.to_editable_permissions();
|
||||
|
||||
std::fs::remove_dir_all(&temp).ok();
|
||||
|
||||
Ok(ExtensionPreview {
|
||||
manifest,
|
||||
is_valid_signature,
|
||||
key_hash,
|
||||
editable_permissions,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn install_extension_with_permissions_internal(
|
||||
&self,
|
||||
app_handle: AppHandle,
|
||||
source_path: String,
|
||||
custom_permissions: EditablePermissions,
|
||||
state: &State<'_, AppState>,
|
||||
) -> Result<String, ExtensionError> {
|
||||
let source = PathBuf::from(&source_path);
|
||||
|
||||
let temp = std::env::temp_dir().join(format!("haexhub_ext_{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
let mut archive =
|
||||
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Invalid ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
archive
|
||||
.extract(&temp)
|
||||
.map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Cannot extract ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest_path = temp.join("manifest.json");
|
||||
let manifest_content =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read manifest: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
let content_hash = ExtensionCrypto::hash_directory(&temp)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
|
||||
ExtensionCrypto::verify_signature(&manifest.public_key, &content_hash, &manifest.signature)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
|
||||
let key_hash = manifest.calculate_key_hash()?;
|
||||
let full_extension_id = format!("{}-{}", key_hash, manifest.id);
|
||||
|
||||
let extensions_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
|
||||
})?
|
||||
.join("extensions")
|
||||
.join(&full_extension_id)
|
||||
.join(&manifest.version);
|
||||
|
||||
std::fs::create_dir_all(&extensions_dir)
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
copy_directory(
|
||||
temp.to_string_lossy().to_string(),
|
||||
extensions_dir.to_string_lossy().to_string(),
|
||||
)?;
|
||||
|
||||
std::fs::remove_dir_all(&temp).ok();
|
||||
|
||||
let permissions = custom_permissions.to_internal_permissions(&full_extension_id);
|
||||
|
||||
let granted_permissions: Vec<_> = permissions
|
||||
.into_iter()
|
||||
.filter(|p| p.status == PermissionStatus::Granted)
|
||||
.collect();
|
||||
|
||||
PermissionManager::save_permissions(state, &full_extension_id, &granted_permissions)
|
||||
.await?;
|
||||
|
||||
let extension = Extension {
|
||||
id: full_extension_id.clone(),
|
||||
name: manifest.name.clone(),
|
||||
source: ExtensionSource::Production {
|
||||
path: extensions_dir.clone(),
|
||||
version: manifest.version.clone(),
|
||||
},
|
||||
manifest: manifest.clone(),
|
||||
enabled: true,
|
||||
last_accessed: SystemTime::now(),
|
||||
};
|
||||
|
||||
self.add_production_extension(extension)?;
|
||||
|
||||
Ok(full_extension_id)
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionState {
|
||||
pub extensions: Mutex<HashMap<String, ExtensionManifest>>,
|
||||
}
|
||||
|
||||
impl ExtensionState {
|
||||
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
|
||||
let mut extensions = self.extensions.lock().unwrap();
|
||||
extensions.insert(path, manifest);
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
|
||||
let extensions = self.extensions.lock().unwrap();
|
||||
extensions.values().find(|p| p.name == addon_id).cloned()
|
||||
}
|
||||
}
|
||||
250
src-tauri/src/extension/core/manifest.rs
Normal file
250
src-tauri/src/extension/core/manifest.rs
Normal file
@ -0,0 +1,250 @@
|
||||
// src-tauri/src/extension/core/manifest.rs
|
||||
|
||||
use crate::extension::crypto::ExtensionCrypto;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::types::{
|
||||
Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints,
|
||||
PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionManifest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub entry: String,
|
||||
pub icon: Option<String>,
|
||||
pub public_key: String,
|
||||
pub signature: String,
|
||||
pub permissions: ExtensionManifestPermissions,
|
||||
pub homepage: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
pub fn calculate_key_hash(&self) -> Result<String, ExtensionError> {
|
||||
ExtensionCrypto::calculate_key_hash(&self.public_key)
|
||||
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })
|
||||
}
|
||||
|
||||
pub fn full_extension_id(&self) -> Result<String, ExtensionError> {
|
||||
let key_hash = self.calculate_key_hash()?;
|
||||
Ok(format!("{}-{}", key_hash, self.id))
|
||||
}
|
||||
|
||||
pub fn to_editable_permissions(&self) -> EditablePermissions {
|
||||
let mut permissions = Vec::new();
|
||||
|
||||
if let Some(db) = &self.permissions.database {
|
||||
for resource in &db.read {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "db".to_string(),
|
||||
action: "read".to_string(),
|
||||
target: resource.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
for resource in &db.write {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "db".to_string(),
|
||||
action: "write".to_string(),
|
||||
target: resource.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(fs) = &self.permissions.filesystem {
|
||||
for path in &fs.read {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "fs".to_string(),
|
||||
action: "read".to_string(),
|
||||
target: path.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
for path in &fs.write {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "fs".to_string(),
|
||||
action: "write".to_string(),
|
||||
target: path.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(http_list) = &self.permissions.http {
|
||||
for domain in http_list {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "http".to_string(),
|
||||
action: "read".to_string(),
|
||||
target: domain.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(shell_list) = &self.permissions.shell {
|
||||
for command in shell_list {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "shell".to_string(),
|
||||
action: "read".to_string(),
|
||||
target: command.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
EditablePermissions { permissions }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct ExtensionManifestPermissions {
|
||||
#[serde(default)]
|
||||
pub database: Option<DatabaseManifestPermissions>,
|
||||
#[serde(default)]
|
||||
pub filesystem: Option<FilesystemManifestPermissions>,
|
||||
#[serde(default)]
|
||||
pub http: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub shell: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct DatabaseManifestPermissions {
|
||||
#[serde(default)]
|
||||
pub read: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub write: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct FilesystemManifestPermissions {
|
||||
#[serde(default)]
|
||||
pub read: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub write: Vec<String>,
|
||||
}
|
||||
|
||||
// Editable Permissions für UI
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct EditablePermissions {
|
||||
pub permissions: Vec<EditablePermission>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct EditablePermission {
|
||||
pub resource_type: String,
|
||||
pub action: String,
|
||||
pub target: String,
|
||||
pub constraints: Option<serde_json::Value>,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl EditablePermissions {
|
||||
pub fn to_internal_permissions(&self, extension_id: &str) -> Vec<ExtensionPermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.map(|p| ExtensionPermission {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
extension_id: extension_id.to_string(),
|
||||
resource_type: match p.resource_type.as_str() {
|
||||
"fs" => ResourceType::Fs,
|
||||
"http" => ResourceType::Http,
|
||||
"db" => ResourceType::Db,
|
||||
"shell" => ResourceType::Shell,
|
||||
_ => ResourceType::Fs,
|
||||
},
|
||||
action: match p.action.as_str() {
|
||||
"read" => Action::Read,
|
||||
"write" => Action::Write,
|
||||
_ => Action::Read,
|
||||
},
|
||||
target: p.target.clone(),
|
||||
constraints: p
|
||||
.constraints
|
||||
.as_ref()
|
||||
.and_then(|c| Self::parse_constraints(&p.resource_type, c)),
|
||||
status: match p.status.as_str() {
|
||||
"granted" => PermissionStatus::Granted,
|
||||
"denied" => PermissionStatus::Denied,
|
||||
"ask" => PermissionStatus::Ask,
|
||||
_ => PermissionStatus::Denied,
|
||||
},
|
||||
haex_timestamp: None,
|
||||
haex_tombstone: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_constraints(
|
||||
resource_type: &str,
|
||||
json_value: &serde_json::Value,
|
||||
) -> Option<PermissionConstraints> {
|
||||
match resource_type {
|
||||
"db" => serde_json::from_value::<DbConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Database),
|
||||
"fs" => serde_json::from_value::<FsConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Filesystem),
|
||||
"http" => serde_json::from_value::<HttpConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Http),
|
||||
"shell" => serde_json::from_value::<ShellConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Shell),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ExtensionPreview {
|
||||
pub manifest: ExtensionManifest,
|
||||
pub is_valid_signature: bool,
|
||||
pub key_hash: String,
|
||||
pub editable_permissions: EditablePermissions,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionInfoResponse {
|
||||
pub key_hash: String,
|
||||
pub name: String,
|
||||
pub full_id: String,
|
||||
pub version: String,
|
||||
pub display_name: Option<String>,
|
||||
pub namespace: Option<String>,
|
||||
pub allowed_origin: String,
|
||||
}
|
||||
|
||||
impl ExtensionInfoResponse {
|
||||
pub fn from_extension(
|
||||
extension: &crate::extension::core::types::Extension,
|
||||
) -> Result<Self, ExtensionError> {
|
||||
use crate::extension::core::types::get_tauri_origin;
|
||||
|
||||
let allowed_origin = get_tauri_origin();
|
||||
let key_hash = extension.manifest.calculate_key_hash()?;
|
||||
let full_id = extension.manifest.full_extension_id()?;
|
||||
|
||||
Ok(Self {
|
||||
key_hash,
|
||||
name: extension.manifest.name.clone(),
|
||||
full_id,
|
||||
version: extension.manifest.version.clone(),
|
||||
display_name: Some(extension.manifest.name.clone()),
|
||||
namespace: extension.manifest.author.clone(),
|
||||
allowed_origin,
|
||||
})
|
||||
}
|
||||
}
|
||||
10
src-tauri/src/extension/core/mod.rs
Normal file
10
src-tauri/src/extension/core/mod.rs
Normal file
@ -0,0 +1,10 @@
|
||||
// src-tauri/src/extension/core/mod.rs
|
||||
|
||||
pub mod manager;
|
||||
pub mod manifest;
|
||||
pub mod protocol;
|
||||
pub mod types;
|
||||
|
||||
pub use manager::*;
|
||||
pub use manifest::*;
|
||||
pub use protocol::*;
|
||||
252
src-tauri/src/extension/core/protocol.rs
Normal file
252
src-tauri/src/extension/core/protocol.rs
Normal file
@ -0,0 +1,252 @@
|
||||
// src-tauri/src/extension/core/protocol.rs
|
||||
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::AppState;
|
||||
use mime;
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tauri::http::{Request, Response};
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ExtensionInfo {
|
||||
id: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DataProcessingError {
|
||||
HexDecoding(hex::FromHexError),
|
||||
Utf8Conversion(std::string::FromUtf8Error),
|
||||
JsonParsing(serde_json::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for DataProcessingError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DataProcessingError::HexDecoding(e) => write!(f, "Hex-Dekodierungsfehler: {}", e),
|
||||
DataProcessingError::Utf8Conversion(e) => {
|
||||
write!(f, "UTF-8-Konvertierungsfehler: {}", e)
|
||||
}
|
||||
DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DataProcessingError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
DataProcessingError::HexDecoding(e) => Some(e),
|
||||
DataProcessingError::Utf8Conversion(e) => Some(e),
|
||||
DataProcessingError::JsonParsing(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hex::FromHexError> for DataProcessingError {
|
||||
fn from(err: hex::FromHexError) -> Self {
|
||||
DataProcessingError::HexDecoding(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for DataProcessingError {
|
||||
fn from(err: std::string::FromUtf8Error) -> Self {
|
||||
DataProcessingError::Utf8Conversion(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for DataProcessingError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
DataProcessingError::JsonParsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_secure_extension_asset_path(
|
||||
app_handle: &AppHandle,
|
||||
state: State<AppState>,
|
||||
extension_id: &str,
|
||||
extension_version: &str,
|
||||
requested_asset_path: &str,
|
||||
) -> Result<PathBuf, ExtensionError> {
|
||||
if extension_id.is_empty()
|
||||
|| !extension_id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-')
|
||||
{
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: format!("Invalid extension ID: {}", extension_id),
|
||||
});
|
||||
}
|
||||
|
||||
if extension_version.is_empty()
|
||||
|| !extension_version
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
|
||||
{
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: format!("Invalid extension version: {}", extension_version),
|
||||
});
|
||||
}
|
||||
|
||||
let specific_extension_dir =
|
||||
state
|
||||
.extension_manager
|
||||
.get_extension_dir(app_handle, extension_id, extension_version)?;
|
||||
|
||||
let clean_relative_path = requested_asset_path
|
||||
.replace('\\', "/")
|
||||
.trim_start_matches('/')
|
||||
.split('/')
|
||||
.filter(|&part| !part.is_empty() && part != "." && part != "..")
|
||||
.collect::<PathBuf>();
|
||||
|
||||
if clean_relative_path.as_os_str().is_empty() && requested_asset_path != "/" {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Empty or invalid asset path".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let final_path = specific_extension_dir.join(clean_relative_path);
|
||||
|
||||
match final_path.canonicalize() {
|
||||
Ok(canonical_path) => {
|
||||
let canonical_base = specific_extension_dir
|
||||
.canonicalize()
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
if canonical_path.starts_with(&canonical_base) {
|
||||
Ok(canonical_path)
|
||||
} else {
|
||||
eprintln!(
|
||||
"SECURITY WARNING: Path traversal attempt blocked: {}",
|
||||
requested_asset_path
|
||||
);
|
||||
Err(ExtensionError::SecurityViolation {
|
||||
reason: format!("Path traversal attempt: {}", requested_asset_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
if final_path.starts_with(&specific_extension_dir) {
|
||||
Ok(final_path)
|
||||
} else {
|
||||
eprintln!(
|
||||
"SECURITY WARNING: Invalid asset path: {}",
|
||||
requested_asset_path
|
||||
);
|
||||
Err(ExtensionError::SecurityViolation {
|
||||
reason: format!("Invalid asset path: {}", requested_asset_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extension_protocol_handler(
|
||||
state: State<AppState>,
|
||||
app_handle: &AppHandle,
|
||||
request: &Request<Vec<u8>>,
|
||||
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
let uri_ref = request.uri();
|
||||
println!("Protokoll Handler für: {}", uri_ref);
|
||||
|
||||
let host = uri_ref
|
||||
.host()
|
||||
.ok_or("Kein Host (Extension ID) in URI gefunden")?
|
||||
.to_string();
|
||||
|
||||
let path_str = uri_ref.path();
|
||||
let segments_iter = path_str.split('/').filter(|s| !s.is_empty());
|
||||
let resource_segments: Vec<&str> = segments_iter.collect();
|
||||
let raw_asset_path = resource_segments.join("/");
|
||||
let asset_to_load = if raw_asset_path.is_empty() {
|
||||
"index.html"
|
||||
} else {
|
||||
&raw_asset_path
|
||||
};
|
||||
|
||||
match process_hex_encoded_json(&host) {
|
||||
Ok(info) => {
|
||||
println!("Daten erfolgreich verarbeitet:");
|
||||
println!(" ID: {}", info.id);
|
||||
println!(" Version: {}", info.version);
|
||||
let absolute_secure_path = resolve_secure_extension_asset_path(
|
||||
app_handle,
|
||||
state,
|
||||
&info.id,
|
||||
&info.version,
|
||||
&asset_to_load,
|
||||
)?;
|
||||
|
||||
println!("absolute_secure_path: {}", absolute_secure_path.display());
|
||||
|
||||
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
|
||||
match fs::read(&absolute_secure_path) {
|
||||
Ok(content) => {
|
||||
let mime_type = mime_guess::from_path(&absolute_secure_path)
|
||||
.first_or(mime::APPLICATION_OCTET_STREAM)
|
||||
.to_string();
|
||||
let content_length = content.len();
|
||||
println!(
|
||||
"Liefere {} ({}, {} bytes) ",
|
||||
absolute_secure_path.display(),
|
||||
mime_type,
|
||||
content_length
|
||||
);
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", mime_type)
|
||||
.header("Content-Length", content_length.to_string())
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.body(content)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Fehler beim Lesen der Datei {}: {}",
|
||||
absolute_secure_path.display(),
|
||||
e
|
||||
);
|
||||
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
|
||||
404
|
||||
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
403
|
||||
} else {
|
||||
500
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.status(status_code)
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"Asset nicht gefunden oder ist kein File: {}",
|
||||
absolute_secure_path.display()
|
||||
);
|
||||
Response::builder()
|
||||
.status(404)
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fehler bei der Datenverarbeitung: {}", e);
|
||||
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_hex_encoded_json(hex_input: &str) -> Result<ExtensionInfo, DataProcessingError> {
|
||||
let bytes = hex::decode(hex_input)?;
|
||||
let json_string = String::from_utf8(bytes)?;
|
||||
let extension_info: ExtensionInfo = serde_json::from_str(&json_string)?;
|
||||
Ok(extension_info)
|
||||
}
|
||||
94
src-tauri/src/extension/core/types.rs
Normal file
94
src-tauri/src/extension/core/types.rs
Normal file
@ -0,0 +1,94 @@
|
||||
// src-tauri/src/extension/core/types.rs
|
||||
|
||||
use crate::extension::core::manifest::ExtensionManifest;
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// Extension source type (production vs development)
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExtensionSource {
|
||||
Production {
|
||||
path: PathBuf,
|
||||
version: String,
|
||||
},
|
||||
Development {
|
||||
dev_server_url: String,
|
||||
manifest_path: PathBuf,
|
||||
auto_reload: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Complete extension data structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Extension {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub source: ExtensionSource,
|
||||
pub manifest: ExtensionManifest,
|
||||
pub enabled: bool,
|
||||
pub last_accessed: SystemTime,
|
||||
}
|
||||
|
||||
pub fn get_tauri_origin() -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
"https://tauri.localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_directory(
|
||||
source: String,
|
||||
destination: String,
|
||||
) -> Result<(), crate::extension::error::ExtensionError> {
|
||||
use crate::extension::error::ExtensionError;
|
||||
use std::path::PathBuf;
|
||||
|
||||
println!(
|
||||
"Kopiere Verzeichnis von '{}' nach '{}'",
|
||||
source, destination
|
||||
);
|
||||
|
||||
let source_path = PathBuf::from(&source);
|
||||
let destination_path = PathBuf::from(&destination);
|
||||
|
||||
if !source_path.exists() || !source_path.is_dir() {
|
||||
return Err(ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("Source directory '{}' not found", source),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let mut options = fs_extra::dir::CopyOptions::new();
|
||||
options.overwrite = true;
|
||||
options.copy_inside = true;
|
||||
options.buffer_size = 64000;
|
||||
|
||||
fs_extra::dir::copy(&source_path, &destination_path, &options).map_err(|e| {
|
||||
ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
973
src-tauri/src/extension/core_old.rs
Normal file
973
src-tauri/src/extension/core_old.rs
Normal file
@ -0,0 +1,973 @@
|
||||
/// src-tauri/src/extension/core.rs
|
||||
use crate::extension::crypto::ExtensionCrypto;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::manager::PermissionManager;
|
||||
use crate::extension::permissions::types::{
|
||||
Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints,
|
||||
PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints,
|
||||
};
|
||||
use crate::AppState;
|
||||
use mime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Digest;
|
||||
use sha2::Sha256;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tauri::State;
|
||||
use tauri::{
|
||||
http::{Request, Response},
|
||||
AppHandle, Manager, Runtime, UriSchemeContext,
|
||||
};
|
||||
use zip::ZipArchive;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ExtensionPreview {
|
||||
pub manifest: ExtensionManifest,
|
||||
pub is_valid_signature: bool,
|
||||
pub key_hash: String,
|
||||
pub editable_permissions: EditablePermissions,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct EditablePermissions {
|
||||
pub permissions: Vec<EditablePermission>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct EditablePermission {
|
||||
pub resource_type: String,
|
||||
pub action: String,
|
||||
pub target: String,
|
||||
pub constraints: Option<serde_json::Value>,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl EditablePermissions {
|
||||
/// Konvertiert EditablePermissions zu internen ExtensionPermissions
|
||||
pub fn to_internal_permissions(&self, extension_id: &str) -> Vec<ExtensionPermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.map(|p| ExtensionPermission {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
extension_id: extension_id.to_string(),
|
||||
resource_type: match p.resource_type.as_str() {
|
||||
"fs" => ResourceType::Fs,
|
||||
"http" => ResourceType::Http,
|
||||
"db" => ResourceType::Db,
|
||||
"shell" => ResourceType::Shell,
|
||||
_ => ResourceType::Fs, // Fallback
|
||||
},
|
||||
action: match p.action.as_str() {
|
||||
"read" => Action::Read,
|
||||
"write" => Action::Write,
|
||||
_ => Action::Read, // Fallback
|
||||
},
|
||||
target: p.target.clone(),
|
||||
constraints: p
|
||||
.constraints
|
||||
.as_ref()
|
||||
.and_then(|c| Self::parse_constraints(&p.resource_type, c)),
|
||||
status: match p.status.as_str() {
|
||||
"granted" => PermissionStatus::Granted,
|
||||
"denied" => PermissionStatus::Denied,
|
||||
"ask" => PermissionStatus::Ask,
|
||||
_ => PermissionStatus::Denied, // Fallback
|
||||
},
|
||||
haex_timestamp: None,
|
||||
haex_tombstone: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_constraints(
|
||||
resource_type: &str,
|
||||
json_value: &serde_json::Value,
|
||||
) -> Option<PermissionConstraints> {
|
||||
match resource_type {
|
||||
"db" => serde_json::from_value::<DbConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Database),
|
||||
"fs" => serde_json::from_value::<FsConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Filesystem),
|
||||
"http" => serde_json::from_value::<HttpConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Http),
|
||||
"shell" => serde_json::from_value::<ShellConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Shell),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Filtert nur granted Permissions
|
||||
pub fn filter_granted(&self) -> Vec<EditablePermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.filter(|p| p.status == "granted")
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionManifest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub entry: String,
|
||||
pub icon: Option<String>,
|
||||
pub public_key: String,
|
||||
pub signature: String,
|
||||
pub permissions: ExtensionManifestPermissions,
|
||||
pub homepage: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
/// Berechnet den Key Hash für diese Extension
|
||||
pub fn calculate_key_hash(&self) -> Result<String, ExtensionError> {
|
||||
ExtensionCrypto::calculate_key_hash(&self.public_key)
|
||||
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })
|
||||
}
|
||||
|
||||
/// Generiert die vollständige Extension ID mit Key Hash Prefix
|
||||
pub fn full_extension_id(&self) -> Result<String, ExtensionError> {
|
||||
let key_hash = self.calculate_key_hash()?;
|
||||
Ok(format!("{}-{}", key_hash, self.id))
|
||||
}
|
||||
pub fn to_editable_permissions(&self) -> EditablePermissions {
|
||||
let mut database = Vec::new();
|
||||
let mut filesystem = Vec::new();
|
||||
let mut http = Vec::new();
|
||||
|
||||
if let Some(db) = &self.permissions.database {
|
||||
for resource in &db.read {
|
||||
database.push(EditableDatabasePermission {
|
||||
operation: "read".to_string(),
|
||||
resource: resource.clone(),
|
||||
status: PermissionStatus::Granted,
|
||||
});
|
||||
}
|
||||
for resource in &db.write {
|
||||
database.push(EditableDatabasePermission {
|
||||
operation: "write".to_string(),
|
||||
resource: resource.clone(),
|
||||
status: PermissionStatus::Granted,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(fs) = &self.permissions.filesystem {
|
||||
for path in &fs.read {
|
||||
filesystem.push(EditableFilesystemPermission {
|
||||
operation: "read".to_string(),
|
||||
path: path.clone(),
|
||||
status: PermissionStatus::Granted,
|
||||
});
|
||||
}
|
||||
for path in &fs.write {
|
||||
filesystem.push(EditableFilesystemPermission {
|
||||
operation: "write".to_string(),
|
||||
path: path.clone(),
|
||||
status: PermissionStatus::Granted,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(http_list) = &self.permissions.http {
|
||||
for domain in http_list {
|
||||
http.push(EditableHttpPermission {
|
||||
domain: domain.clone(),
|
||||
status: PermissionStatus::Granted,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
EditablePermissions {
|
||||
database,
|
||||
filesystem,
|
||||
http,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
/// Konvertiert Manifest zu EditablePermissions (neue Version)
|
||||
pub fn to_editable_permissions(&self) -> EditablePermissions {
|
||||
let mut permissions = Vec::new();
|
||||
|
||||
// Database Permissions
|
||||
if let Some(db) = &self.permissions.database {
|
||||
for resource in &db.read {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "db".to_string(),
|
||||
action: "read".to_string(),
|
||||
target: resource.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
for resource in &db.write {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "db".to_string(),
|
||||
action: "write".to_string(),
|
||||
target: resource.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Filesystem Permissions
|
||||
if let Some(fs) = &self.permissions.filesystem {
|
||||
for path in &fs.read {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "fs".to_string(),
|
||||
action: "read".to_string(),
|
||||
target: path.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
for path in &fs.write {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "fs".to_string(),
|
||||
action: "write".to_string(),
|
||||
target: path.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP Permissions
|
||||
if let Some(http_list) = &self.permissions.http {
|
||||
for domain in http_list {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "http".to_string(),
|
||||
action: "read".to_string(), // HTTP ist meist read
|
||||
target: domain.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Shell Permissions
|
||||
if let Some(shell_list) = &self.permissions.shell {
|
||||
for command in shell_list {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "shell".to_string(),
|
||||
action: "read".to_string(), // Shell hat keine action mehr im Schema
|
||||
target: command.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
EditablePermissions { permissions }
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionInfoResponse {
|
||||
pub key_hash: String,
|
||||
pub name: String,
|
||||
pub full_id: String,
|
||||
pub version: String,
|
||||
pub display_name: Option<String>,
|
||||
pub namespace: Option<String>,
|
||||
pub allowed_origin: String,
|
||||
}
|
||||
|
||||
impl ExtensionInfoResponse {
|
||||
pub fn from_extension(extension: &Extension) -> Result<Self, ExtensionError> {
|
||||
// Bestimme die allowed_origin basierend auf Tauri-Konfiguration
|
||||
let allowed_origin = get_tauri_origin();
|
||||
let key_hash = extension
|
||||
.manifest
|
||||
.calculate_key_hash()
|
||||
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })?;
|
||||
let full_id = extension
|
||||
.manifest
|
||||
.full_extension_id()
|
||||
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })?;
|
||||
|
||||
Ok(Self {
|
||||
key_hash,
|
||||
name: extension.manifest.name.clone(),
|
||||
full_id,
|
||||
version: extension.manifest.version.clone(),
|
||||
display_name: Some(extension.manifest.name.clone()),
|
||||
namespace: extension.manifest.author.clone(),
|
||||
allowed_origin,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tauri_origin() -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
"https://tauri.localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension source type (production vs development)
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExtensionSource {
|
||||
Production {
|
||||
path: PathBuf,
|
||||
version: String,
|
||||
},
|
||||
Development {
|
||||
dev_server_url: String,
|
||||
manifest_path: PathBuf,
|
||||
auto_reload: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Complete extension data structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Extension {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub source: ExtensionSource,
|
||||
pub manifest: ExtensionManifest,
|
||||
pub enabled: bool,
|
||||
pub last_accessed: SystemTime,
|
||||
}
|
||||
|
||||
/// Cached permission data for performance
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedPermission {
|
||||
pub permissions: Vec<DbExtensionPermission>,
|
||||
pub cached_at: SystemTime,
|
||||
pub ttl: Duration,
|
||||
}
|
||||
|
||||
/// Enhanced extension manager
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionManager {
|
||||
pub production_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub dev_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
|
||||
}
|
||||
|
||||
impl ExtensionManager {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn get_base_extension_dir(&self, app_handle: AppHandle) -> Result<PathBuf, ExtensionError> {
|
||||
let path = app_handle
|
||||
.path()
|
||||
.app_local_data_dir() // Korrekt für Ressourcen
|
||||
// Wenn du stattdessen App Local Data willst: .app_local_data_dir()
|
||||
.map_err(|e| ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
|
||||
})?
|
||||
.join("extensions");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn get_extension_dir(
|
||||
&self,
|
||||
app_handle: AppHandle,
|
||||
extension_id: &str,
|
||||
extension_version: &str,
|
||||
) -> Result<PathBuf, ExtensionError> {
|
||||
let specific_extension_dir = self
|
||||
.get_base_extension_dir(app_handle)?
|
||||
.join(extension_id)
|
||||
.join(extension_version);
|
||||
|
||||
Ok(specific_extension_dir)
|
||||
}
|
||||
|
||||
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Validate filesystem permissions
|
||||
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
|
||||
fs_perms.validate()?;
|
||||
}
|
||||
*/
|
||||
match &extension.source {
|
||||
ExtensionSource::Production { .. } => {
|
||||
let mut extensions = self.production_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Production source".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_dev_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Validate filesystem permissions
|
||||
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
|
||||
fs_perms.validate()?;
|
||||
} */
|
||||
|
||||
match &extension.source {
|
||||
ExtensionSource::Development { .. } => {
|
||||
let mut extensions = self.dev_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Development source".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, extension_id: &str) -> Option<Extension> {
|
||||
// Dev extensions take priority
|
||||
let dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if let Some(extension) = dev_extensions.get(extension_id) {
|
||||
return Some(extension.clone());
|
||||
}
|
||||
|
||||
// Then check production
|
||||
let prod_extensions = self.production_extensions.lock().unwrap();
|
||||
prod_extensions.get(extension_id).cloned()
|
||||
}
|
||||
|
||||
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
|
||||
{
|
||||
let mut dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if dev_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut prod_extensions = self.production_extensions.lock().unwrap();
|
||||
if prod_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ExtensionError::NotFound {
|
||||
id: extension_id.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn remove_extension_internal(
|
||||
&self,
|
||||
app_handle: AppHandle,
|
||||
extension_id: String,
|
||||
extension_version: String,
|
||||
state: &State<'_, AppState>,
|
||||
) -> Result<(), ExtensionError> {
|
||||
// Permissions löschen (verwendet jetzt die neue Methode)
|
||||
PermissionManager::delete_permissions(state, &extension_id).await?;
|
||||
|
||||
// Extension aus Manager entfernen
|
||||
self.remove_extension(&extension_id)?;
|
||||
|
||||
let extension_dir =
|
||||
self.get_extension_dir(app_handle, &extension_id, &extension_version)?;
|
||||
|
||||
// Dateien löschen
|
||||
if extension_dir.exists() {
|
||||
std::fs::remove_dir_all(&extension_dir)
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn preview_extension_internal(
|
||||
&self,
|
||||
source_path: String,
|
||||
) -> Result<ExtensionPreview, ExtensionError> {
|
||||
let source = PathBuf::from(&source_path);
|
||||
|
||||
// ZIP in temp entpacken
|
||||
let temp = std::env::temp_dir().join(format!("haexhub_preview_{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
let mut archive =
|
||||
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Invalid ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
archive
|
||||
.extract(&temp)
|
||||
.map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Cannot extract ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
// Manifest laden
|
||||
let manifest_path = temp.join("manifest.json");
|
||||
let manifest_content =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read manifest: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
// Signatur verifizieren
|
||||
let content_hash = ExtensionCrypto::hash_directory(&temp)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
|
||||
let is_valid_signature = ExtensionCrypto::verify_signature(
|
||||
&manifest.public_key,
|
||||
&content_hash,
|
||||
&manifest.signature,
|
||||
)
|
||||
.is_ok();
|
||||
|
||||
let key_hash = manifest.calculate_key_hash()?;
|
||||
|
||||
// Editable permissions erstellen
|
||||
let editable_permissions = manifest.to_editable_permissions();
|
||||
|
||||
// Cleanup
|
||||
std::fs::remove_dir_all(&temp).ok();
|
||||
|
||||
Ok(ExtensionPreview {
|
||||
manifest,
|
||||
is_valid_signature,
|
||||
key_hash,
|
||||
editable_permissions,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn install_extension_with_permissions_internal(
|
||||
&self,
|
||||
app_handle: AppHandle,
|
||||
source_path: String,
|
||||
custom_permissions: EditablePermissions,
|
||||
state: &State<'_, AppState>,
|
||||
) -> Result<String, ExtensionError> {
|
||||
let source = PathBuf::from(&source_path);
|
||||
|
||||
// 1. ZIP entpacken
|
||||
let temp = std::env::temp_dir().join(format!("haexhub_ext_{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
let mut archive =
|
||||
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Invalid ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
archive
|
||||
.extract(&temp)
|
||||
.map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Cannot extract ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
// 2. Manifest laden
|
||||
let manifest_path = temp.join("manifest.json");
|
||||
let manifest_content =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read manifest: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
// 3. Signatur verifizieren
|
||||
let content_hash = ExtensionCrypto::hash_directory(&temp)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
|
||||
ExtensionCrypto::verify_signature(&manifest.public_key, &content_hash, &manifest.signature)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
|
||||
// 4. Key Hash berechnen
|
||||
let key_hash = manifest.calculate_key_hash()?;
|
||||
let full_extension_id = format!("{}-{}", key_hash, manifest.id);
|
||||
|
||||
// 5. Zielverzeichnis
|
||||
let extensions_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
|
||||
})?
|
||||
.join("extensions")
|
||||
.join(&full_extension_id)
|
||||
.join(&manifest.version);
|
||||
|
||||
std::fs::create_dir_all(&extensions_dir)
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
// 6. Dateien kopieren
|
||||
copy_directory(
|
||||
temp.to_string_lossy().to_string(),
|
||||
extensions_dir.to_string_lossy().to_string(),
|
||||
)?;
|
||||
|
||||
// 7. Temp aufräumen
|
||||
std::fs::remove_dir_all(&temp).ok();
|
||||
|
||||
// 8. Custom Permissions konvertieren und speichern
|
||||
let permissions = custom_permissions.to_internal_permissions(&full_extension_id);
|
||||
let granted_permissions = permissions.filter_granted();
|
||||
PermissionManager::save_permissions(&state.db, &granted_permissions).await?;
|
||||
|
||||
// 9. Extension registrieren
|
||||
let extension = Extension {
|
||||
id: full_extension_id.clone(),
|
||||
name: manifest.name.clone(),
|
||||
source: ExtensionSource::Production {
|
||||
path: extensions_dir.clone(),
|
||||
version: manifest.version.clone(),
|
||||
},
|
||||
manifest: manifest.clone(),
|
||||
enabled: true,
|
||||
last_accessed: SystemTime::now(),
|
||||
};
|
||||
|
||||
state
|
||||
.extension_manager
|
||||
.add_production_extension(extension)?;
|
||||
|
||||
Ok(full_extension_id)
|
||||
}
|
||||
}
|
||||
|
||||
// For backward compatibility
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionState {
|
||||
pub extensions: Mutex<HashMap<String, ExtensionManifest>>,
|
||||
}
|
||||
|
||||
impl ExtensionState {
|
||||
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
|
||||
let mut extensions = self.extensions.lock().unwrap();
|
||||
extensions.insert(path, manifest);
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
|
||||
let extensions = self.extensions.lock().unwrap();
|
||||
extensions.values().find(|p| p.name == addon_id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ExtensionInfo {
|
||||
id: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DataProcessingError {
|
||||
HexDecoding(hex::FromHexError),
|
||||
Utf8Conversion(std::string::FromUtf8Error),
|
||||
JsonParsing(serde_json::Error),
|
||||
}
|
||||
|
||||
// Implementierung von Display für benutzerfreundliche Fehlermeldungen
|
||||
impl fmt::Display for DataProcessingError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DataProcessingError::HexDecoding(e) => write!(f, "Hex-Dekodierungsfehler: {}", e),
|
||||
DataProcessingError::Utf8Conversion(e) => {
|
||||
write!(f, "UTF-8-Konvertierungsfehler: {}", e)
|
||||
}
|
||||
DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implementierung von std::error::Error (optional, aber gute Praxis für bibliotheksähnlichen Code)
|
||||
impl std::error::Error for DataProcessingError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
DataProcessingError::HexDecoding(e) => Some(e),
|
||||
DataProcessingError::Utf8Conversion(e) => Some(e),
|
||||
DataProcessingError::JsonParsing(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implementierung von From-Traits für einfache Verwendung des '?'-Operators
|
||||
impl From<hex::FromHexError> for DataProcessingError {
|
||||
fn from(err: hex::FromHexError) -> Self {
|
||||
DataProcessingError::HexDecoding(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for DataProcessingError {
|
||||
fn from(err: std::string::FromUtf8Error) -> Self {
|
||||
DataProcessingError::Utf8Conversion(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for DataProcessingError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
DataProcessingError::JsonParsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_directory(source: String, destination: String) -> Result<(), ExtensionError> {
|
||||
println!(
|
||||
"Kopiere Verzeichnis von '{}' nach '{}'",
|
||||
source, destination
|
||||
);
|
||||
|
||||
let source_path = PathBuf::from(&source);
|
||||
let destination_path = PathBuf::from(&destination);
|
||||
|
||||
if !source_path.exists() || !source_path.is_dir() {
|
||||
return Err(ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("Source directory '{}' not found", source),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Optionen für fs_extra::dir::copy
|
||||
let mut options = fs_extra::dir::CopyOptions::new();
|
||||
options.overwrite = true; // Überschreibe Zieldateien, falls sie existieren
|
||||
options.copy_inside = true; // Kopiere den *Inhalt* des Quellordners in den Zielordner
|
||||
// options.content_only = true; // Alternative: nur Inhalt kopieren, Zielordner muss existieren
|
||||
options.buffer_size = 64000; // Standard-Puffergröße, kann angepasst werden
|
||||
|
||||
// Führe die Kopieroperation aus
|
||||
fs_extra::dir::copy(&source_path, &destination_path, &options).map_err(|e| {
|
||||
ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resolve_secure_extension_asset_path(
|
||||
app_handle: AppHandle,
|
||||
state: State<AppState>,
|
||||
extension_id: &str,
|
||||
extension_version: &str,
|
||||
requested_asset_path: &str,
|
||||
) -> Result<PathBuf, ExtensionError> {
|
||||
// 1. Validiere die Extension ID
|
||||
if extension_id.is_empty()
|
||||
|| !extension_id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-')
|
||||
{
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: format!("Invalid extension ID: {}", extension_id),
|
||||
});
|
||||
}
|
||||
|
||||
if extension_version.is_empty()
|
||||
|| !extension_version
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
|
||||
{
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: format!("Invalid extension version: {}", extension_version),
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Verzeichnis für die spezifische Erweiterung
|
||||
let specific_extension_dir =
|
||||
state
|
||||
.extension_manager
|
||||
.get_extension_dir(app_handle, extension_id, extension_version)?;
|
||||
|
||||
// 4. Bereinige den angeforderten Asset-Pfad
|
||||
let clean_relative_path = requested_asset_path
|
||||
.replace('\\', "/")
|
||||
.trim_start_matches('/')
|
||||
.split('/')
|
||||
.filter(|&part| !part.is_empty() && part != "." && part != "..")
|
||||
.collect::<PathBuf>();
|
||||
|
||||
if clean_relative_path.as_os_str().is_empty() && requested_asset_path != "/" {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Empty or invalid asset path".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Setze den finalen Pfad zusammen
|
||||
let final_path = specific_extension_dir.join(clean_relative_path);
|
||||
|
||||
// 6. SICHERHEITSCHECK
|
||||
match final_path.canonicalize() {
|
||||
Ok(canonical_path) => {
|
||||
let canonical_base = specific_extension_dir
|
||||
.canonicalize()
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
if canonical_path.starts_with(&canonical_base) {
|
||||
Ok(canonical_path)
|
||||
} else {
|
||||
eprintln!( /* ... Sicherheitswarnung ... */ );
|
||||
Err(ExtensionError::SecurityViolation {
|
||||
reason: format!("Path traversal attempt: {}", requested_asset_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Fehler bei canonicalize (z.B. Pfad existiert nicht)
|
||||
if final_path.starts_with(&specific_extension_dir) {
|
||||
Ok(final_path) // Nicht-kanonisierten Pfad zurückgeben
|
||||
} else {
|
||||
eprintln!( /* ... Sicherheitswarnung ... */ );
|
||||
Err(ExtensionError::SecurityViolation {
|
||||
reason: format!("Invalid asset path: {}", requested_asset_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extension_protocol_handler<R: Runtime>(
|
||||
state: State<AppState>,
|
||||
app_handle: AppHandle,
|
||||
request: &Request<Vec<u8>>,
|
||||
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
let uri_ref = request.uri();
|
||||
println!("Protokoll Handler für: {}", uri_ref);
|
||||
|
||||
let host = uri_ref
|
||||
.host()
|
||||
.ok_or("Kein Host (Extension ID) in URI gefunden")?
|
||||
.to_string();
|
||||
|
||||
let path_str = uri_ref.path();
|
||||
let segments_iter = path_str.split('/').filter(|s| !s.is_empty());
|
||||
let resource_segments: Vec<&str> = segments_iter.collect();
|
||||
let raw_asset_path = resource_segments.join("/");
|
||||
let asset_to_load = if raw_asset_path.is_empty() {
|
||||
"index.html"
|
||||
} else {
|
||||
&raw_asset_path
|
||||
};
|
||||
|
||||
match process_hex_encoded_json(&host) {
|
||||
Ok(info) => {
|
||||
println!("Daten erfolgreich verarbeitet:");
|
||||
println!(" ID: {}", info.id);
|
||||
println!(" Version: {}", info.version);
|
||||
let absolute_secure_path = resolve_secure_extension_asset_path(
|
||||
app_handle,
|
||||
state,
|
||||
&info.id,
|
||||
&info.version,
|
||||
&asset_to_load,
|
||||
)?;
|
||||
|
||||
println!("absolute_secure_path: {}", absolute_secure_path.display());
|
||||
|
||||
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
|
||||
match fs::read(&absolute_secure_path) {
|
||||
Ok(content) => {
|
||||
let mime_type = mime_guess::from_path(&absolute_secure_path)
|
||||
.first_or(mime::APPLICATION_OCTET_STREAM)
|
||||
.to_string();
|
||||
let content_length = content.len();
|
||||
println!(
|
||||
"Liefere {} ({}, {} bytes) ", // Content-Length zum Log hinzugefügt
|
||||
absolute_secure_path.display(),
|
||||
mime_type,
|
||||
content_length
|
||||
);
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", mime_type)
|
||||
.header("Content-Length", content_length.to_string()) // <-- HIER HINZUGEFÜGT
|
||||
// Optional, aber gut für Streaming-Fähigkeit:
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.body(content)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Fehler beim Lesen der Datei {}: {}",
|
||||
absolute_secure_path.display(),
|
||||
e
|
||||
);
|
||||
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
|
||||
404
|
||||
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
403
|
||||
} else {
|
||||
500
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.status(status_code)
|
||||
.body(Vec::new()) // Leerer Body für Fehler
|
||||
.map_err(|e| e.into()) // Wandle http::Error in Box<dyn Error> um
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Datei nicht gefunden oder es ist keine Datei
|
||||
eprintln!(
|
||||
"Asset nicht gefunden oder ist kein File: {}",
|
||||
absolute_secure_path.display()
|
||||
);
|
||||
Response::builder()
|
||||
.status(404) // HTTP 404 Not Found
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fehler bei der Datenverarbeitung: {}", e);
|
||||
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.body(Vec::new()) // Leerer Body für Fehler
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_hex_encoded_json(hex_input: &str) -> Result<ExtensionInfo, DataProcessingError> {
|
||||
// Schritt 1: Hex-String zu Bytes dekodieren
|
||||
let bytes = hex::decode(hex_input)?; // Konvertiert hex::FromHexError automatisch
|
||||
|
||||
// Schritt 2: Bytes zu UTF-8-String konvertieren
|
||||
let json_string = String::from_utf8(bytes)?; // Konvertiert FromUtf8Error automatisch
|
||||
|
||||
// Schritt 3: JSON-String zu Struktur parsen
|
||||
let extension_info: ExtensionInfo = serde_json::from_str(&json_string)?; // Konvertiert serde_json::Error automatisch
|
||||
|
||||
Ok(extension_info)
|
||||
}
|
||||
74
src-tauri/src/extension/crypto.rs
Normal file
74
src-tauri/src/extension/crypto.rs
Normal file
@ -0,0 +1,74 @@
|
||||
// src-tauri/src/extension/crypto.rs
|
||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub struct ExtensionCrypto;
|
||||
|
||||
impl ExtensionCrypto {
|
||||
/// Berechnet Hash vom Public Key (wie im SDK)
|
||||
pub fn calculate_key_hash(public_key_hex: &str) -> Result<String, String> {
|
||||
let public_key_bytes =
|
||||
hex::decode(public_key_hex).map_err(|e| format!("Invalid public key hex: {}", e))?;
|
||||
|
||||
let public_key = VerifyingKey::from_bytes(&public_key_bytes.try_into().unwrap())
|
||||
.map_err(|e| format!("Invalid public key: {}", e))?;
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(public_key.as_bytes());
|
||||
let result = hasher.finalize();
|
||||
|
||||
// Ersten 20 Hex-Zeichen (10 Bytes) - wie im SDK
|
||||
Ok(hex::encode(&result[..10]))
|
||||
}
|
||||
|
||||
/// Verifiziert Extension-Signatur
|
||||
pub fn verify_signature(
|
||||
public_key_hex: &str,
|
||||
content_hash_hex: &str,
|
||||
signature_hex: &str,
|
||||
) -> Result<(), String> {
|
||||
let public_key_bytes =
|
||||
hex::decode(public_key_hex).map_err(|e| format!("Invalid public key: {}", e))?;
|
||||
let public_key = VerifyingKey::from_bytes(&public_key_bytes.try_into().unwrap())
|
||||
.map_err(|e| format!("Invalid public key: {}", e))?;
|
||||
|
||||
let signature_bytes =
|
||||
hex::decode(signature_hex).map_err(|e| format!("Invalid signature: {}", e))?;
|
||||
let signature = Signature::from_bytes(&signature_bytes.try_into().unwrap());
|
||||
|
||||
let content_hash =
|
||||
hex::decode(content_hash_hex).map_err(|e| format!("Invalid content hash: {}", e))?;
|
||||
|
||||
public_key
|
||||
.verify(&content_hash, &signature)
|
||||
.map_err(|e| format!("Signature verification failed: {}", e))
|
||||
}
|
||||
|
||||
/// Berechnet Hash eines Verzeichnisses (für Verifikation)
|
||||
pub fn hash_directory(dir: &std::path::Path) -> Result<String, String> {
|
||||
use std::fs;
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
let mut entries: Vec<_> = fs::read_dir(dir)
|
||||
.map_err(|e| format!("Cannot read directory: {}", e))?
|
||||
.filter_map(|e| e.ok())
|
||||
.collect();
|
||||
|
||||
// Sortieren für deterministische Hashes
|
||||
entries.sort_by_key(|e| e.path());
|
||||
|
||||
for entry in entries {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let content = fs::read(&path)
|
||||
.map_err(|e| format!("Cannot read file {}: {}", path.display(), e))?;
|
||||
hasher.update(&content);
|
||||
} else if path.is_dir() {
|
||||
let subdir_hash = Self::hash_directory(&path)?;
|
||||
hasher.update(hex::decode(&subdir_hash).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(hex::encode(hasher.finalize()))
|
||||
}
|
||||
}
|
||||
153
src-tauri/src/extension/database/executor.rs
Normal file
153
src-tauri/src/extension/database/executor.rs
Normal file
@ -0,0 +1,153 @@
|
||||
// src-tauri/src/extension/database/executor.rs (neu)
|
||||
|
||||
use crate::crdt::hlc::HlcService;
|
||||
use crate::crdt::transformer::CrdtTransformer;
|
||||
use crate::crdt::trigger;
|
||||
use crate::database::core::{parse_sql_statements, ValueConverter};
|
||||
use crate::database::error::DatabaseError;
|
||||
use rusqlite::{params_from_iter, Transaction};
|
||||
use serde_json::Value as JsonValue;
|
||||
use sqlparser::ast::Statement;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// SQL-Executor OHNE Berechtigungsprüfung - für interne Nutzung
|
||||
pub struct SqlExecutor;
|
||||
|
||||
impl SqlExecutor {
|
||||
/// Führt SQL aus (mit CRDT-Transformation) - OHNE Permission-Check
|
||||
pub fn execute_internal(
|
||||
tx: &Transaction,
|
||||
hlc_service: &HlcService,
|
||||
sql: &str,
|
||||
params: &[JsonValue],
|
||||
) -> Result<HashSet<String>, DatabaseError> {
|
||||
// Parameter validation
|
||||
let total_placeholders = sql.matches('?').count();
|
||||
if total_placeholders != params.len() {
|
||||
return Err(DatabaseError::ParameterMismatchError {
|
||||
expected: total_placeholders,
|
||||
provided: params.len(),
|
||||
sql: sql.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// SQL parsing
|
||||
let mut ast_vec = parse_sql_statements(sql)?;
|
||||
|
||||
let transformer = CrdtTransformer::new();
|
||||
|
||||
// Generate HLC timestamp
|
||||
let hlc_timestamp =
|
||||
hlc_service
|
||||
.new_timestamp_and_persist(tx)
|
||||
.map_err(|e| DatabaseError::HlcError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
// Transform statements
|
||||
let mut modified_schema_tables = HashSet::new();
|
||||
for statement in &mut ast_vec {
|
||||
if let Some(table_name) =
|
||||
transformer.transform_execute_statement(statement, &hlc_timestamp)?
|
||||
{
|
||||
modified_schema_tables.insert(table_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert parameters
|
||||
let sql_values = ValueConverter::convert_params(params)?;
|
||||
|
||||
// Execute statements
|
||||
for statement in ast_vec {
|
||||
let sql_str = statement.to_string();
|
||||
|
||||
tx.execute(&sql_str, params_from_iter(sql_values.iter()))
|
||||
.map_err(|e| DatabaseError::ExecutionError {
|
||||
sql: sql_str.clone(),
|
||||
table: None,
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
if let Statement::CreateTable(create_table_details) = statement {
|
||||
let table_name_str = create_table_details.name.to_string();
|
||||
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(modified_schema_tables)
|
||||
}
|
||||
|
||||
/// Führt SELECT aus (mit CRDT-Transformation) - OHNE Permission-Check
|
||||
pub fn select_internal(
|
||||
conn: &rusqlite::Connection,
|
||||
sql: &str,
|
||||
params: &[JsonValue],
|
||||
) -> Result<Vec<JsonValue>, DatabaseError> {
|
||||
// Parameter validation
|
||||
let total_placeholders = sql.matches('?').count();
|
||||
if total_placeholders != params.len() {
|
||||
return Err(DatabaseError::ParameterMismatchError {
|
||||
expected: total_placeholders,
|
||||
provided: params.len(),
|
||||
sql: sql.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut ast_vec = parse_sql_statements(sql)?;
|
||||
|
||||
if ast_vec.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// Validate that all statements are queries
|
||||
for stmt in &ast_vec {
|
||||
if !matches!(stmt, Statement::Query(_)) {
|
||||
return Err(DatabaseError::ExecutionError {
|
||||
sql: sql.to_string(),
|
||||
reason: "Only SELECT statements are allowed".to_string(),
|
||||
table: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let sql_params = ValueConverter::convert_params(params)?;
|
||||
let transformer = CrdtTransformer::new();
|
||||
|
||||
let last_statement = ast_vec.pop().unwrap();
|
||||
let mut stmt_to_execute = last_statement;
|
||||
|
||||
transformer.transform_select_statement(&mut stmt_to_execute)?;
|
||||
let transformed_sql = stmt_to_execute.to_string();
|
||||
|
||||
let mut prepared_stmt =
|
||||
conn.prepare(&transformed_sql)
|
||||
.map_err(|e| DatabaseError::ExecutionError {
|
||||
sql: transformed_sql.clone(),
|
||||
reason: e.to_string(),
|
||||
table: None,
|
||||
})?;
|
||||
|
||||
let column_names: Vec<String> = prepared_stmt
|
||||
.column_names()
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
let rows = prepared_stmt
|
||||
.query_map(params_from_iter(sql_params.iter()), |row| {
|
||||
crate::extension::database::row_to_json_value(row, &column_names)
|
||||
})
|
||||
.map_err(|e| DatabaseError::QueryError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for row_result in rows {
|
||||
results.push(row_result.map_err(|e| DatabaseError::RowProcessingError {
|
||||
reason: e.to_string(),
|
||||
})?);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,15 @@
|
||||
// src-tauri/src/extension/database/mod.rs
|
||||
|
||||
pub mod permissions;
|
||||
pub mod executor;
|
||||
use crate::crdt::hlc::HlcService;
|
||||
use crate::crdt::transformer::CrdtTransformer;
|
||||
use crate::crdt::trigger;
|
||||
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::validator::SqlPermissionValidator;
|
||||
use crate::AppState;
|
||||
use permissions::{check_read_permission, check_write_permission};
|
||||
|
||||
use rusqlite::params_from_iter;
|
||||
use rusqlite::types::Value as SqlValue;
|
||||
use rusqlite::Transaction;
|
||||
@ -116,7 +117,7 @@ pub async fn extension_sql_execute(
|
||||
hlc_service: State<'_, HlcService>,
|
||||
) -> Result<Vec<String>, ExtensionError> {
|
||||
// Permission check
|
||||
check_write_permission(&state.db, &extension_id, sql).await?;
|
||||
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
|
||||
|
||||
// Parameter validation
|
||||
validate_params(sql, ¶ms)?;
|
||||
@ -186,7 +187,7 @@ pub async fn extension_sql_select(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<JsonValue>, ExtensionError> {
|
||||
// Permission check
|
||||
check_read_permission(&state.db, &extension_id, sql).await?;
|
||||
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
|
||||
|
||||
// Parameter validation
|
||||
validate_params(sql, ¶ms)?;
|
||||
|
||||
@ -1,278 +0,0 @@
|
||||
// src-tauri/src/extension/database/permissions.rs
|
||||
|
||||
use crate::database::core::{
|
||||
extract_table_names_from_sql, parse_single_statement, with_connection,
|
||||
};
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::database::DbConnection;
|
||||
use crate::extension::error::ExtensionError;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlparser::ast::{Statement, TableFactor, TableObject};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DbExtensionPermission {
|
||||
pub id: String,
|
||||
pub extension_id: String,
|
||||
pub resource: String,
|
||||
pub operation: String,
|
||||
}
|
||||
|
||||
/// Prüft Leseberechtigungen für eine Extension
|
||||
pub async fn check_read_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
sql: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
|
||||
reason: e.to_string(),
|
||||
sql: sql.to_string(),
|
||||
})?;
|
||||
|
||||
match statement {
|
||||
Statement::Query(query) => {
|
||||
let tables = extract_table_names_from_sql(&query.to_string())?;
|
||||
check_table_permissions(connection, extension_id, &tables, "read").await
|
||||
}
|
||||
_ => Err(DatabaseError::UnsupportedStatement {
|
||||
reason: "Only SELECT statements are allowed for read operations".to_string(),
|
||||
sql: sql.to_string(),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Prüft Schreibberechtigungen für eine Extension
|
||||
pub async fn check_write_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
sql: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
|
||||
reason: e.to_string(),
|
||||
sql: sql.to_string(),
|
||||
})?;
|
||||
|
||||
match statement {
|
||||
Statement::Insert(insert) => {
|
||||
let table_name = extract_table_name_from_insert(&insert)?;
|
||||
check_single_table_permission(connection, extension_id, &table_name, "write").await
|
||||
}
|
||||
Statement::Update { table, .. } => {
|
||||
let table_name = extract_table_name_from_table_factor(&table.relation)?;
|
||||
check_single_table_permission(connection, extension_id, &table_name, "write").await
|
||||
}
|
||||
Statement::Delete(delete) => {
|
||||
// DELETE wird durch CRDT-Transform zu UPDATE mit tombstone = 1
|
||||
let table_name = extract_table_name_from_delete(&delete)?;
|
||||
check_single_table_permission(connection, extension_id, &table_name, "write").await
|
||||
}
|
||||
Statement::CreateTable(create_table) => {
|
||||
let table_name = create_table.name.to_string();
|
||||
check_single_table_permission(connection, extension_id, &table_name, "create").await
|
||||
}
|
||||
Statement::AlterTable { name, .. } => {
|
||||
let table_name = name.to_string();
|
||||
check_single_table_permission(connection, extension_id, &table_name, "alter").await
|
||||
}
|
||||
Statement::Drop { names, .. } => {
|
||||
// Für DROP können mehrere Tabellen angegeben sein
|
||||
let table_names: Vec<String> = names.iter().map(|name| name.to_string()).collect();
|
||||
check_table_permissions(connection, extension_id, &table_names, "drop").await
|
||||
}
|
||||
_ => Err(DatabaseError::UnsupportedStatement {
|
||||
reason: "SQL Statement is not allowed".to_string(),
|
||||
sql: sql.to_string(),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrahiert Tabellenname aus INSERT-Statement
|
||||
fn extract_table_name_from_insert(
|
||||
insert: &sqlparser::ast::Insert,
|
||||
) -> Result<String, ExtensionError> {
|
||||
match &insert.table {
|
||||
TableObject::TableName(name) => Ok(name.to_string()),
|
||||
_ => Err(DatabaseError::NoTableError {
|
||||
sql: insert.to_string(),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrahiert Tabellenname aus TableFactor
|
||||
fn extract_table_name_from_table_factor(
|
||||
table_factor: &TableFactor,
|
||||
) -> Result<String, ExtensionError> {
|
||||
match table_factor {
|
||||
TableFactor::Table { name, .. } => Ok(name.to_string()),
|
||||
_ => Err(DatabaseError::StatementError {
|
||||
reason: "Complex table references not supported".to_string(),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrahiert Tabellenname aus DELETE-Statement
|
||||
fn extract_table_name_from_delete(
|
||||
delete: &sqlparser::ast::Delete,
|
||||
) -> Result<String, ExtensionError> {
|
||||
use sqlparser::ast::FromTable;
|
||||
|
||||
let table_name = match &delete.from {
|
||||
FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => {
|
||||
if !tables.is_empty() {
|
||||
extract_table_name_from_table_factor(&tables[0].relation)?
|
||||
} else if !delete.tables.is_empty() {
|
||||
delete.tables[0].to_string()
|
||||
} else {
|
||||
return Err(DatabaseError::NoTableError {
|
||||
sql: delete.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(table_name)
|
||||
}
|
||||
|
||||
/// Prüft Berechtigung für eine einzelne Tabelle
|
||||
async fn check_single_table_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
table_name: &str,
|
||||
operation: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
check_table_permissions(
|
||||
connection,
|
||||
extension_id,
|
||||
&[table_name.to_string()],
|
||||
operation,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Prüft Berechtigungen für mehrere Tabellen
|
||||
async fn check_table_permissions(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
table_names: &[String],
|
||||
operation: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions =
|
||||
get_extension_permissions(connection, extension_id, "database", operation).await?;
|
||||
|
||||
for table_name in table_names {
|
||||
let has_permission = permissions
|
||||
.iter()
|
||||
.any(|perm| perm.resource.contains(table_name));
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
operation,
|
||||
&format!("table '{}'", table_name),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ruft die Berechtigungen einer Extension aus der Datenbank ab
|
||||
pub async fn get_extension_permissions(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
resource: &str,
|
||||
operation: &str,
|
||||
) -> Result<Vec<DbExtensionPermission>, DatabaseError> {
|
||||
with_connection(connection, |conn| {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT id, extension_id, resource, operation, path
|
||||
FROM haex_vault_extension_permissions
|
||||
WHERE extension_id = ?1 AND resource = ?2 AND operation = ?3",
|
||||
)
|
||||
.map_err(|e| DatabaseError::PrepareError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
let rows = stmt
|
||||
.query_map([extension_id, resource, operation], |row| {
|
||||
Ok(DbExtensionPermission {
|
||||
id: row.get(0)?,
|
||||
extension_id: row.get(1)?,
|
||||
resource: row.get(2)?,
|
||||
operation: row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| DatabaseError::QueryError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
let mut permissions = Vec::new();
|
||||
for row_result in rows {
|
||||
let permission = row_result.map_err(|e| DatabaseError::DatabaseError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
permissions.push(permission);
|
||||
}
|
||||
|
||||
Ok(permissions)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::extension::error::ExtensionError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_single_statement() {
|
||||
let sql = "SELECT * FROM users";
|
||||
let result = parse_single_statement(sql);
|
||||
assert!(result.is_ok());
|
||||
assert!(matches!(result.unwrap(), Statement::Query(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_sql() {
|
||||
let sql = "INVALID SQL";
|
||||
let result = parse_single_statement(sql);
|
||||
// parse_single_statement gibt DatabaseError zurück, nicht DatabaseError
|
||||
assert!(result.is_err());
|
||||
// Wenn du spezifischer sein möchtest, kannst du den DatabaseError-Typ prüfen:
|
||||
match result {
|
||||
Err(DatabaseError::ParseError { .. }) => {
|
||||
// Test erfolgreich - wir haben einen ParseError erhalten
|
||||
}
|
||||
Err(other) => {
|
||||
// Andere DatabaseError-Varianten sind auch akzeptabel für ungültiges SQL
|
||||
println!("Received other DatabaseError: {:?}", other);
|
||||
}
|
||||
Ok(_) => panic!("Expected error for invalid SQL"),
|
||||
}
|
||||
}
|
||||
|
||||
/* #[test]
|
||||
fn test_permission_error_access_denied() {
|
||||
let error = ExtensionError::access_denied("ext1", "read", "table1", "not allowed");
|
||||
match error {
|
||||
ExtensionError::AccessDenied {
|
||||
extension_id,
|
||||
operation,
|
||||
resource,
|
||||
reason,
|
||||
} => {
|
||||
assert_eq!(extension_id, "ext1");
|
||||
assert_eq!(operation, "read");
|
||||
assert_eq!(resource, "table1");
|
||||
assert_eq!(reason, "not allowed");
|
||||
}
|
||||
_ => panic!("Expected AccessDenied error"),
|
||||
}
|
||||
} */
|
||||
}
|
||||
@ -1,9 +1,36 @@
|
||||
/// src-tauri/src/extension/error.rs
|
||||
// src-tauri/src/extension/error.rs
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::database::error::DatabaseError;
|
||||
|
||||
/// Comprehensive error type for extension operations
|
||||
/// Error codes for frontend handling
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ExtensionErrorCode {
|
||||
SecurityViolation = 1000,
|
||||
NotFound = 1001,
|
||||
PermissionDenied = 1002,
|
||||
Database = 2000,
|
||||
Filesystem = 2001,
|
||||
Http = 2002,
|
||||
Shell = 2003,
|
||||
Manifest = 3000,
|
||||
Validation = 3001,
|
||||
InvalidPublicKey = 4000,
|
||||
InvalidSignature = 4001,
|
||||
SignatureVerificationFailed = 4002,
|
||||
CalculateHash = 4003,
|
||||
Installation = 5000,
|
||||
}
|
||||
|
||||
impl serde::Serialize for ExtensionErrorCode {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_u16(*self as u16)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExtensionError {
|
||||
#[error("Security violation: {reason}")]
|
||||
@ -29,15 +56,10 @@ pub enum ExtensionError {
|
||||
Filesystem {
|
||||
#[from]
|
||||
source: std::io::Error,
|
||||
// oder: source: FilesystemError,
|
||||
},
|
||||
|
||||
#[error("HTTP request failed: {reason}")]
|
||||
Http {
|
||||
reason: String,
|
||||
#[source]
|
||||
source: Option<Box<dyn std::error::Error + Send + Sync>>,
|
||||
},
|
||||
Http { reason: String },
|
||||
|
||||
#[error("Shell command failed: {reason}")]
|
||||
Shell {
|
||||
@ -45,29 +67,51 @@ pub enum ExtensionError {
|
||||
exit_code: Option<i32>,
|
||||
},
|
||||
|
||||
/* #[error("IO error: {source}")]
|
||||
Io {
|
||||
#[from]
|
||||
source: std::io::Error,
|
||||
}, */
|
||||
#[error("Manifest error: {reason}")]
|
||||
ManifestError { reason: String },
|
||||
|
||||
#[error("Validation error: {reason}")]
|
||||
ValidationError { reason: String },
|
||||
|
||||
#[error("Dev server error: {reason}")]
|
||||
DevServerError { reason: String },
|
||||
#[error("Invalid Public Key: {reason}")]
|
||||
InvalidPublicKey { reason: String },
|
||||
|
||||
#[error("Serialization error: {reason}")]
|
||||
SerializationError { reason: String },
|
||||
#[error("Invalid Signature: {reason}")]
|
||||
InvalidSignature { reason: String },
|
||||
|
||||
#[error("Configuration error: {reason}")]
|
||||
ConfigError { reason: String },
|
||||
#[error("Error during hash calculation: {reason}")]
|
||||
CalculateHashError { reason: String },
|
||||
|
||||
#[error("Signature verification failed: {reason}")]
|
||||
SignatureVerificationFailed { reason: String },
|
||||
|
||||
#[error("Extension installation failed: {reason}")]
|
||||
InstallationFailed { reason: String },
|
||||
}
|
||||
|
||||
impl ExtensionError {
|
||||
/// Convenience constructor for permission denied errors
|
||||
/// Get error code for this error
|
||||
pub fn code(&self) -> ExtensionErrorCode {
|
||||
match self {
|
||||
ExtensionError::SecurityViolation { .. } => ExtensionErrorCode::SecurityViolation,
|
||||
ExtensionError::NotFound { .. } => ExtensionErrorCode::NotFound,
|
||||
ExtensionError::PermissionDenied { .. } => ExtensionErrorCode::PermissionDenied,
|
||||
ExtensionError::Database { .. } => ExtensionErrorCode::Database,
|
||||
ExtensionError::Filesystem { .. } => ExtensionErrorCode::Filesystem,
|
||||
ExtensionError::Http { .. } => ExtensionErrorCode::Http,
|
||||
ExtensionError::Shell { .. } => ExtensionErrorCode::Shell,
|
||||
ExtensionError::ManifestError { .. } => ExtensionErrorCode::Manifest,
|
||||
ExtensionError::ValidationError { .. } => ExtensionErrorCode::Validation,
|
||||
ExtensionError::InvalidPublicKey { .. } => ExtensionErrorCode::InvalidPublicKey,
|
||||
ExtensionError::InvalidSignature { .. } => ExtensionErrorCode::InvalidSignature,
|
||||
ExtensionError::SignatureVerificationFailed { .. } => {
|
||||
ExtensionErrorCode::SignatureVerificationFailed
|
||||
}
|
||||
ExtensionError::InstallationFailed { .. } => ExtensionErrorCode::Installation,
|
||||
ExtensionError::CalculateHashError { .. } => ExtensionErrorCode::CalculateHash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn permission_denied(extension_id: &str, operation: &str, resource: &str) -> Self {
|
||||
Self::PermissionDenied {
|
||||
extension_id: extension_id.to_string(),
|
||||
@ -76,34 +120,6 @@ impl ExtensionError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience constructor for HTTP errors
|
||||
pub fn http_error(reason: &str) -> Self {
|
||||
Self::Http {
|
||||
reason: reason.to_string(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience constructor for HTTP errors with source
|
||||
pub fn http_error_with_source(
|
||||
reason: &str,
|
||||
source: Box<dyn std::error::Error + Send + Sync>,
|
||||
) -> Self {
|
||||
Self::Http {
|
||||
reason: reason.to_string(),
|
||||
source: Some(source),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience constructor for shell errors
|
||||
pub fn shell_error(reason: &str, exit_code: Option<i32>) -> Self {
|
||||
Self::Shell {
|
||||
reason: reason.to_string(),
|
||||
exit_code,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this error is related to permissions
|
||||
pub fn is_permission_error(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
@ -111,11 +127,9 @@ impl ExtensionError {
|
||||
)
|
||||
}
|
||||
|
||||
/// Extract extension ID if available
|
||||
pub fn extension_id(&self) -> Option<&str> {
|
||||
match self {
|
||||
ExtensionError::PermissionDenied { extension_id, .. } => Some(extension_id),
|
||||
ExtensionError::Database { source } => source.extension_id(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -128,29 +142,12 @@ impl serde::Serialize for ExtensionError {
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
|
||||
let mut state = serializer.serialize_struct("ExtensionError", 3)?;
|
||||
let mut state = serializer.serialize_struct("ExtensionError", 4)?;
|
||||
|
||||
// Error type as discriminator
|
||||
let error_type = match self {
|
||||
ExtensionError::SecurityViolation { .. } => "SecurityViolation",
|
||||
ExtensionError::NotFound { .. } => "NotFound",
|
||||
ExtensionError::PermissionDenied { .. } => "PermissionDenied",
|
||||
ExtensionError::Database { .. } => "Database",
|
||||
ExtensionError::Filesystem { .. } => "Filesystem",
|
||||
ExtensionError::Http { .. } => "Http",
|
||||
ExtensionError::Shell { .. } => "Shell",
|
||||
//ExtensionError::Io { .. } => "Io",
|
||||
ExtensionError::ManifestError { .. } => "ManifestError",
|
||||
ExtensionError::ValidationError { .. } => "ValidationError",
|
||||
ExtensionError::DevServerError { .. } => "DevServerError",
|
||||
ExtensionError::SerializationError { .. } => "SerializationError",
|
||||
ExtensionError::ConfigError { .. } => "ConfigError",
|
||||
};
|
||||
|
||||
state.serialize_field("type", error_type)?;
|
||||
state.serialize_field("code", &self.code())?;
|
||||
state.serialize_field("type", &format!("{:?}", self))?;
|
||||
state.serialize_field("message", &self.to_string())?;
|
||||
|
||||
// Add extension_id if available
|
||||
if let Some(ext_id) = self.extension_id() {
|
||||
state.serialize_field("extension_id", ext_id)?;
|
||||
} else {
|
||||
@ -161,54 +158,16 @@ impl serde::Serialize for ExtensionError {
|
||||
}
|
||||
}
|
||||
|
||||
// For Tauri command serialization
|
||||
impl From<ExtensionError> for String {
|
||||
fn from(error: ExtensionError) -> Self {
|
||||
serde_json::to_string(&error).unwrap_or_else(|_| error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ExtensionError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ExtensionError::SerializationError {
|
||||
ExtensionError::ManifestError {
|
||||
reason: err.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::database::error::DatabaseError;
|
||||
|
||||
/* #[test]
|
||||
fn test_database_error_conversion() {
|
||||
let db_error = DatabaseError::access_denied("ext1", "read", "users", "no permission");
|
||||
let ext_error: ExtensionError = db_error.into();
|
||||
|
||||
assert!(ext_error.is_permission_error());
|
||||
assert_eq!(ext_error.extension_id(), Some("ext1"));
|
||||
} */
|
||||
|
||||
#[test]
|
||||
fn test_permission_denied_constructor() {
|
||||
let error = ExtensionError::permission_denied("ext1", "write", "config.json");
|
||||
|
||||
match error {
|
||||
ExtensionError::PermissionDenied {
|
||||
extension_id,
|
||||
operation,
|
||||
resource,
|
||||
} => {
|
||||
assert_eq!(extension_id, "ext1");
|
||||
assert_eq!(operation, "write");
|
||||
assert_eq!(resource, "config.json");
|
||||
}
|
||||
_ => panic!("Expected PermissionDenied error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
let error = ExtensionError::permission_denied("ext1", "read", "database");
|
||||
let serialized = serde_json::to_string(&error).unwrap();
|
||||
|
||||
// Basic check that it serializes properly
|
||||
assert!(serialized.contains("PermissionDenied"));
|
||||
assert!(serialized.contains("ext1"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,184 @@
|
||||
use crate::extension::core::{ExtensionInfoResponse, ExtensionManager};
|
||||
use tauri::State;
|
||||
/// src-tauri/src/extension/mod.rs
|
||||
use crate::{
|
||||
extension::{
|
||||
core::{EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
||||
error::ExtensionError,
|
||||
},
|
||||
AppState,
|
||||
};
|
||||
use tauri::{AppHandle, State};
|
||||
pub mod core;
|
||||
pub mod crypto;
|
||||
pub mod database;
|
||||
pub mod error;
|
||||
pub mod filesystem;
|
||||
pub mod permission_manager;
|
||||
pub mod permissions;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_extension_info(
|
||||
extension_id: String,
|
||||
extension_manager: State<ExtensionManager>,
|
||||
state: State<AppState>,
|
||||
) -> Result<ExtensionInfoResponse, String> {
|
||||
let extension = extension_manager
|
||||
let extension = state
|
||||
.extension_manager
|
||||
.get_extension(&extension_id)
|
||||
.ok_or_else(|| format!("Extension nicht gefunden: {}", extension_id))?;
|
||||
|
||||
Ok(ExtensionInfoResponse::from_extension(&extension))
|
||||
ExtensionInfoResponse::from_extension(&extension).map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_all_extensions(state: State<AppState>) -> Result<Vec<ExtensionInfoResponse>, String> {
|
||||
let mut extensions = Vec::new();
|
||||
|
||||
// Production Extensions
|
||||
{
|
||||
let prod_exts = state
|
||||
.extension_manager
|
||||
.production_extensions
|
||||
.lock()
|
||||
.unwrap();
|
||||
for ext in prod_exts.values() {
|
||||
extensions.push(ExtensionInfoResponse::from_extension(ext)?);
|
||||
}
|
||||
}
|
||||
|
||||
// Dev Extensions
|
||||
{
|
||||
let dev_exts = state.extension_manager.dev_extensions.lock().unwrap();
|
||||
for ext in dev_exts.values() {
|
||||
extensions.push(ExtensionInfoResponse::from_extension(ext)?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(extensions)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn preview_extension(
|
||||
state: State<'_, AppState>,
|
||||
source_path: String,
|
||||
) -> Result<ExtensionPreview, ExtensionError> {
|
||||
state
|
||||
.extension_manager
|
||||
.preview_extension_internal(source_path)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_extension_with_permissions(
|
||||
app_handle: AppHandle,
|
||||
source_path: String,
|
||||
custom_permissions: EditablePermissions,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, ExtensionError> {
|
||||
state
|
||||
.extension_manager
|
||||
.install_extension_with_permissions_internal(
|
||||
app_handle,
|
||||
source_path,
|
||||
custom_permissions,
|
||||
&state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
/* #[tauri::command]
|
||||
pub async fn install_extension(
|
||||
app_handle: AppHandle,
|
||||
source_path: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, String> {
|
||||
let source = PathBuf::from(&source_path);
|
||||
|
||||
// Manifest laden
|
||||
let manifest_path = source.join("manifest.json");
|
||||
let manifest_content = std::fs::read_to_string(&manifest_path)
|
||||
.map_err(|e| format!("Manifest konnte nicht gelesen werden: {}", e))?;
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)
|
||||
.map_err(|e| format!("Manifest ist ungültig: {}", e))?;
|
||||
|
||||
// Signatur verifizieren
|
||||
let content_hash = ExtensionCrypto::hash_directory(&source)?;
|
||||
ExtensionCrypto::verify_signature(&manifest.public_key, &content_hash, &manifest.signature)?;
|
||||
|
||||
// Key Hash berechnen
|
||||
let key_hash = manifest.calculate_key_hash()?;
|
||||
let full_extension_id = format!("{}-{}", key_hash, manifest.id);
|
||||
|
||||
// Zielverzeichnis mit Key Hash Prefix
|
||||
let extensions_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| format!("App-Datenverzeichnis nicht gefunden: {}", e))?
|
||||
.join("extensions")
|
||||
.join(&full_extension_id) // <- z.B. "a3f5b9c2d1e8f4-haex-pass"
|
||||
.join(&manifest.version);
|
||||
|
||||
// Extension-Dateien kopieren
|
||||
std::fs::create_dir_all(&extensions_dir)
|
||||
.map_err(|e| format!("Verzeichnis konnte nicht erstellt werden: {}", e))?;
|
||||
|
||||
let source_to_copy = if source.join("dist").exists() {
|
||||
source.join("dist") // Kopiere aus dist/
|
||||
} else {
|
||||
source.clone() // Kopiere direkt
|
||||
};
|
||||
|
||||
copy_directory(
|
||||
source_to_copy.to_string_lossy().to_string(),
|
||||
extensions_dir.to_string_lossy().to_string(),
|
||||
)?;
|
||||
|
||||
// Permissions speichern
|
||||
let permissions = manifest.to_internal_permissions();
|
||||
PermissionManager::save_permissions(&state.db, &permissions)
|
||||
.await
|
||||
.map_err(|e| format!("Fehler beim Speichern der Permissions: {:?}", e))?;
|
||||
|
||||
// Extension registrieren
|
||||
let extension = Extension {
|
||||
id: full_extension_id.clone(),
|
||||
name: manifest.name.clone(),
|
||||
source: ExtensionSource::Production {
|
||||
path: extensions_dir.clone(),
|
||||
version: manifest.version.clone(),
|
||||
},
|
||||
manifest: manifest.clone(),
|
||||
enabled: true,
|
||||
last_accessed: SystemTime::now(),
|
||||
};
|
||||
|
||||
state
|
||||
.extension_manager
|
||||
.add_production_extension(extension)
|
||||
.map_err(|e| format!("Extension konnte nicht hinzugefügt werden: {:?}", e))?;
|
||||
|
||||
Ok(full_extension_id)
|
||||
}
|
||||
*/
|
||||
#[tauri::command]
|
||||
pub async fn remove_extension(
|
||||
app_handle: AppHandle,
|
||||
extension_id: String,
|
||||
extension_version: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ExtensionError> {
|
||||
state
|
||||
.extension_manager
|
||||
.remove_extension_internal(&app_handle, extension_id, extension_version, &state)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn is_extension_installed(
|
||||
extension_id: String,
|
||||
extension_version: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<bool, String> {
|
||||
if let Some(ext) = state.extension_manager.get_extension(&extension_id) {
|
||||
Ok(ext.manifest.version == extension_version)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,297 +0,0 @@
|
||||
/// src-tauri/src/extension/permission_manager.rs
|
||||
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::database::DbConnection;
|
||||
use crate::extension::database::permissions::DbExtensionPermission;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::Url;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ExtensionPermissions {
|
||||
pub database: Vec<DbExtensionPermission>,
|
||||
pub filesystem: Vec<FilesystemPermission>,
|
||||
pub http: Vec<HttpPermission>,
|
||||
pub shell: Vec<ShellPermission>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct FilesystemPermission {
|
||||
pub extension_id: String,
|
||||
pub operation: String, // read, write, create, delete
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct HttpPermission {
|
||||
pub extension_id: String,
|
||||
pub operation: String, // get, post, put, delete
|
||||
pub domain: String,
|
||||
pub path_pattern: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ShellPermission {
|
||||
pub extension_id: String,
|
||||
pub command: String,
|
||||
pub arguments: Vec<String>,
|
||||
}
|
||||
|
||||
/// Zentraler Permission Manager
|
||||
pub struct PermissionManager;
|
||||
|
||||
impl PermissionManager {
|
||||
/// Prüft Datenbankberechtigungen
|
||||
pub async fn check_database_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
operation: &str,
|
||||
table_name: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions = Self::get_database_permissions(connection, extension_id, operation).await?;
|
||||
|
||||
let has_permission = permissions
|
||||
.iter()
|
||||
.any(|perm| perm.resource.contains(table_name));
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
operation,
|
||||
&format!("database table '{}'", table_name),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft Dateisystem-Berechtigungen
|
||||
pub async fn check_filesystem_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
operation: &str,
|
||||
file_path: &Path,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions = Self::get_filesystem_permissions(connection, extension_id, operation).await?;
|
||||
|
||||
let file_path_str = file_path.to_string_lossy();
|
||||
let has_permission = permissions.iter().any(|perm| {
|
||||
// Prüfe, ob der Pfad mit einem erlaubten Pfad beginnt oder übereinstimmt
|
||||
file_path_str.starts_with(&perm.path) ||
|
||||
// Oder ob es ein Wildcard-Match gibt
|
||||
Self::matches_path_pattern(&perm.path, &file_path_str)
|
||||
});
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
operation,
|
||||
&format!("filesystem path '{}'", file_path_str),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft HTTP-Berechtigungen
|
||||
pub async fn check_http_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
method: &str,
|
||||
url: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions = Self::get_http_permissions(connection, extension_id, method).await?;
|
||||
|
||||
let url_parsed = Url::parse(url).map_err(|e| {
|
||||
ExtensionError::ValidationError {
|
||||
reason: format!("Invalid URL: {}", e),
|
||||
}
|
||||
})?;
|
||||
|
||||
let domain = url_parsed.host_str().unwrap_or("");
|
||||
let path = url_parsed.path();
|
||||
|
||||
let has_permission = permissions.iter().any(|perm| {
|
||||
// Prüfe Domain
|
||||
let domain_matches = perm.domain == "*" ||
|
||||
perm.domain == domain ||
|
||||
domain.ends_with(&format!(".{}", perm.domain));
|
||||
|
||||
// Prüfe Pfad (falls spezifiziert)
|
||||
let path_matches = perm.path_pattern.as_ref()
|
||||
.map(|pattern| Self::matches_path_pattern(pattern, path))
|
||||
.unwrap_or(true);
|
||||
|
||||
domain_matches && path_matches
|
||||
});
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
method,
|
||||
&format!("HTTP request to '{}'", url),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft Shell-Berechtigungen
|
||||
pub async fn check_shell_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
command: &str,
|
||||
args: &[String],
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions = Self::get_shell_permissions(connection, extension_id).await?;
|
||||
|
||||
let has_permission = permissions.iter().any(|perm| {
|
||||
// Prüfe Command
|
||||
if perm.command != command && perm.command != "*" {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prüfe Arguments (falls spezifiziert)
|
||||
if !perm.arguments.is_empty() {
|
||||
// Alle erforderlichen Args müssen vorhanden sein
|
||||
perm.arguments.iter().all(|required_arg| {
|
||||
args.iter().any(|actual_arg| {
|
||||
required_arg == actual_arg || required_arg == "*"
|
||||
})
|
||||
})
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
"execute",
|
||||
&format!("shell command '{}' with args {:?}", command, args),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Private Helper-Methoden
|
||||
|
||||
async fn get_database_permissions(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
operation: &str,
|
||||
) -> Result<Vec<DbExtensionPermission>, ExtensionError> {
|
||||
// Verwende die bestehende Funktion aus dem permissions.rs
|
||||
crate::extension::database::permissions::get_extension_permissions(
|
||||
connection,
|
||||
extension_id,
|
||||
"database",
|
||||
operation
|
||||
).await.map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
async fn get_filesystem_permissions(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
operation: &str,
|
||||
) -> Result<Vec<FilesystemPermission>, ExtensionError> {
|
||||
// Implementierung für Filesystem-Permissions
|
||||
// Ähnlich wie get_database_permissions, aber für filesystem Tabelle
|
||||
todo!("Implementiere Filesystem-Permission-Loading")
|
||||
}
|
||||
|
||||
async fn get_http_permissions(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
method: &str,
|
||||
) -> Result<Vec<HttpPermission>, ExtensionError> {
|
||||
// Implementierung für HTTP-Permissions
|
||||
todo!("Implementiere HTTP-Permission-Loading")
|
||||
}
|
||||
|
||||
async fn get_shell_permissions(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
) -> Result<Vec<ShellPermission>, ExtensionError> {
|
||||
// Implementierung für Shell-Permissions
|
||||
todo!("Implementiere Shell-Permission-Loading")
|
||||
}
|
||||
|
||||
fn matches_path_pattern(pattern: &str, path: &str) -> bool {
|
||||
// Einfache Wildcard-Implementierung
|
||||
if pattern.ends_with('*') {
|
||||
let prefix = &pattern[..pattern.len() - 1];
|
||||
path.starts_with(prefix)
|
||||
} else if pattern.starts_with('*') {
|
||||
let suffix = &pattern[1..];
|
||||
path.ends_with(suffix)
|
||||
} else {
|
||||
pattern == path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience-Funktionen für die verschiedenen Subsysteme
|
||||
impl PermissionManager {
|
||||
/// Convenience für Datei lesen
|
||||
pub async fn can_read_file(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
file_path: &Path,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_filesystem_permission(connection, extension_id, "read", file_path).await
|
||||
}
|
||||
|
||||
/// Convenience für Datei schreiben
|
||||
pub async fn can_write_file(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
file_path: &Path,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_filesystem_permission(connection, extension_id, "write", file_path).await
|
||||
}
|
||||
|
||||
/// Convenience für HTTP GET
|
||||
pub async fn can_http_get(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
url: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_http_permission(connection, extension_id, "GET", url).await
|
||||
}
|
||||
|
||||
/// Convenience für HTTP POST
|
||||
pub async fn can_http_post(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
url: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_http_permission(connection, extension_id, "POST", url).await
|
||||
}
|
||||
|
||||
/// Convenience für Shell-Befehl
|
||||
pub async fn can_execute_command(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
command: &str,
|
||||
args: &[String],
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_shell_permission(connection, extension_id, command, args).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_path_pattern_matching() {
|
||||
assert!(PermissionManager::matches_path_pattern("/home/user/*", "/home/user/documents/file.txt"));
|
||||
assert!(PermissionManager::matches_path_pattern("*.txt", "/path/to/file.txt"));
|
||||
assert!(PermissionManager::matches_path_pattern("/exact/path", "/exact/path"));
|
||||
|
||||
assert!(!PermissionManager::matches_path_pattern("/home/user/*", "/etc/passwd"));
|
||||
assert!(!PermissionManager::matches_path_pattern("*.txt", "/path/to/file.pdf"));
|
||||
}
|
||||
}
|
||||
650
src-tauri/src/extension/permissions/manager.rs
Normal file
650
src-tauri/src/extension/permissions/manager.rs
Normal file
@ -0,0 +1,650 @@
|
||||
use crate::AppState;
|
||||
use crate::database::core::with_connection;
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::extension::database::executor::SqlExecutor;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::types::{Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints, PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints};
|
||||
use serde_json;
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
use tauri::State;
|
||||
use url::Url;
|
||||
|
||||
pub struct PermissionManager;
|
||||
|
||||
impl PermissionManager {
|
||||
/// Speichert alle Permissions einer Extension
|
||||
pub async fn save_permissions(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
permissions: &[ExtensionPermission],
|
||||
) -> Result<(), ExtensionError> {
|
||||
with_connection(&app_state.db, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
|
||||
let hlc_service = app_state
|
||||
.hlc
|
||||
.lock()
|
||||
.map_err(|_| DatabaseError::MutexPoisoned {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
|
||||
for perm in permissions {
|
||||
let resource_type_str = format!("{:?}", perm.resource_type).to_lowercase();
|
||||
let action_str = format!("{:?}", perm.action).to_lowercase();
|
||||
|
||||
let constraints_json = perm
|
||||
.constraints
|
||||
.as_ref()
|
||||
.map(|c| serde_json::to_string(c).ok())
|
||||
.flatten();
|
||||
|
||||
let sql = "INSERT INTO haex_extension_permissions
|
||||
(id, extension_id, resource_type, action, target, constraints, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
let params = vec![
|
||||
json!(perm.id),
|
||||
json!(extension_id),
|
||||
json!(resource_type_str),
|
||||
json!(action_str),
|
||||
json!(perm.target),
|
||||
json!(constraints_json),
|
||||
json!(perm.status.as_str()),
|
||||
];
|
||||
|
||||
SqlExecutor::execute_internal(&tx, &hlc_service, sql, ¶ms)?;
|
||||
}
|
||||
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
/// Aktualisiert eine Permission
|
||||
pub async fn update_permission(
|
||||
app_state: &State<'_, AppState>,
|
||||
permission: &ExtensionPermission,
|
||||
) -> Result<(), ExtensionError> {
|
||||
with_connection(&app_state.db, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
|
||||
let hlc_service = app_state
|
||||
.hlc
|
||||
.lock()
|
||||
.map_err(|_| DatabaseError::MutexPoisoned {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
|
||||
let resource_type_str = format!("{:?}", permission.resource_type).to_lowercase();
|
||||
let action_str = format!("{:?}", permission.action).to_lowercase();
|
||||
|
||||
let constraints_json = permission
|
||||
.constraints
|
||||
.as_ref()
|
||||
.map(|c| serde_json::to_string(c).ok())
|
||||
.flatten();
|
||||
|
||||
let sql = "UPDATE haex_extension_permissions
|
||||
SET resource_type = ?, action = ?, target = ?, constraints = ?, status = ?
|
||||
WHERE id = ?";
|
||||
|
||||
let params = vec![
|
||||
json!(resource_type_str),
|
||||
json!(action_str),
|
||||
json!(permission.target),
|
||||
json!(constraints_json),
|
||||
json!(permission.status.as_str()),
|
||||
json!(permission.id),
|
||||
];
|
||||
|
||||
SqlExecutor::execute_internal(&tx, &hlc_service, sql, ¶ms)?;
|
||||
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
/// Ändert den Status einer Permission
|
||||
pub async fn update_permission_status(
|
||||
app_state: &State<'_, AppState>,
|
||||
permission_id: &str,
|
||||
new_status: PermissionStatus,
|
||||
) -> Result<(), ExtensionError> {
|
||||
with_connection(&app_state.db, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
|
||||
let hlc_service = app_state
|
||||
.hlc
|
||||
.lock()
|
||||
.map_err(|_| DatabaseError::MutexPoisoned {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
|
||||
let sql = "UPDATE haex_extension_permissions
|
||||
SET status = ?
|
||||
WHERE id = ?";
|
||||
|
||||
let params = vec![json!(new_status.as_str()), json!(permission_id)];
|
||||
|
||||
SqlExecutor::execute_internal(&tx, &hlc_service, sql, ¶ms)?;
|
||||
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
/// Löscht alle Permissions einer Extension
|
||||
pub async fn delete_permission(
|
||||
app_state: &State<'_, AppState>,
|
||||
permission_id: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
with_connection(&app_state.db, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
|
||||
let hlc_service = app_state.hlc.lock()
|
||||
.map_err(|_| DatabaseError::MutexPoisoned {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
|
||||
// Echtes DELETE - wird vom CrdtTransformer zu UPDATE umgewandelt
|
||||
let sql = "DELETE FROM haex_extension_permissions WHERE id = ?";
|
||||
|
||||
let params = vec![json!(permission_id)];
|
||||
|
||||
SqlExecutor::execute_internal(&tx, &hlc_service, sql, ¶ms)?;
|
||||
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
Ok(())
|
||||
}).map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
/// Löscht alle Permissions einer Extension (Soft-Delete)
|
||||
pub async fn delete_permissions(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
with_connection(&app_state.db, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
|
||||
let hlc_service = app_state.hlc.lock()
|
||||
.map_err(|_| DatabaseError::MutexPoisoned {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
|
||||
// Echtes DELETE - wird vom CrdtTransformer zu UPDATE umgewandelt
|
||||
let sql = "DELETE FROM haex_extension_permissions WHERE extension_id = ?";
|
||||
|
||||
let params = vec![json!(extension_id)];
|
||||
|
||||
SqlExecutor::execute_internal(&tx, &hlc_service, sql, ¶ms)?;
|
||||
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
Ok(())
|
||||
}).map_err(ExtensionError::from)
|
||||
}
|
||||
/// Lädt alle Permissions einer Extension
|
||||
pub async fn get_permissions(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
) -> Result<Vec<ExtensionPermission>, ExtensionError> {
|
||||
with_connection(&app_state.db, |conn| {
|
||||
let sql = "SELECT id, extension_id, resource_type, action, target, constraints, status, haex_timestamp, haex_tombstone
|
||||
FROM haex_extension_permissions
|
||||
WHERE extension_id = ?";
|
||||
|
||||
let params = vec![json!(extension_id)];
|
||||
|
||||
// SELECT nutzt select_internal
|
||||
let results = SqlExecutor::select_internal(conn, sql, ¶ms)?;
|
||||
|
||||
// Parse JSON results zu ExtensionPermission
|
||||
let permissions = results
|
||||
.into_iter()
|
||||
.map(|row| Self::parse_permission_from_json(row))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(permissions)
|
||||
}).map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
// Helper für JSON -> ExtensionPermission Konvertierung
|
||||
fn parse_permission_from_json(json: serde_json::Value) -> Result<ExtensionPermission, DatabaseError> {
|
||||
|
||||
let obj = json.as_object().ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Expected JSON object".to_string(),
|
||||
})?;
|
||||
|
||||
let resource_type = Self::parse_resource_type(
|
||||
obj.get("resource_type")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing resource_type".to_string(),
|
||||
})?
|
||||
)?;
|
||||
|
||||
let action = Self::parse_action(
|
||||
obj.get("action")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing action".to_string(),
|
||||
})?
|
||||
)?;
|
||||
|
||||
let status = PermissionStatus::from_str(
|
||||
obj.get("status")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing status".to_string(),
|
||||
})?
|
||||
)?; // Jetzt funktioniert das ?
|
||||
|
||||
let constraints = obj.get("constraints")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|json_str| Self::parse_constraints(&resource_type, json_str))
|
||||
.transpose()?;
|
||||
|
||||
Ok(ExtensionPermission {
|
||||
id: obj.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing id".to_string(),
|
||||
})?
|
||||
.to_string(),
|
||||
extension_id: obj.get("extension_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing extension_id".to_string(),
|
||||
})?
|
||||
.to_string(),
|
||||
resource_type,
|
||||
action,
|
||||
target: obj.get("target")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing target".to_string(),
|
||||
})?
|
||||
.to_string(),
|
||||
constraints,
|
||||
status,
|
||||
haex_timestamp: obj.get("haex_timestamp")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
haex_tombstone: obj.get("haex_tombstone")
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|i| i == 1),
|
||||
})
|
||||
}
|
||||
|
||||
/// Prüft Datenbankberechtigungen
|
||||
pub async fn check_database_permission(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
action: Action,
|
||||
table_name: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions = Self::get_permissions(app_state, extension_id).await?;
|
||||
|
||||
let has_permission = permissions
|
||||
.iter()
|
||||
.filter(|perm| perm.status == PermissionStatus::Granted) // NUR granted!
|
||||
.filter(|perm| perm.resource_type == ResourceType::Db)
|
||||
.filter(|perm| perm.action == action) // action ist nicht mehr Option
|
||||
.any(|perm| {
|
||||
if perm.target != "*" && perm.target != table_name {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
});
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
&format!("{:?}", action),
|
||||
&format!("database table '{}'", table_name),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft Dateisystem-Berechtigungen
|
||||
pub async fn check_filesystem_permission(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
action: Action,
|
||||
file_path: &Path,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions = Self::get_permissions(app_state, extension_id).await?;
|
||||
|
||||
let file_path_str = file_path.to_string_lossy();
|
||||
|
||||
let has_permission = permissions
|
||||
.iter()
|
||||
.filter(|perm| perm.status == PermissionStatus::Granted)
|
||||
.filter(|perm| perm.resource_type == ResourceType::Fs)
|
||||
.filter(|perm| perm.action == action)
|
||||
.any(|perm| {
|
||||
if !Self::matches_path_pattern(&perm.target, &file_path_str) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(PermissionConstraints::Filesystem(constraints)) = &perm.constraints {
|
||||
if let Some(allowed_ext) = &constraints.allowed_extensions {
|
||||
if let Some(ext) = file_path.extension() {
|
||||
let ext_str = format!(".{}", ext.to_string_lossy());
|
||||
if !allowed_ext.contains(&ext_str) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
&format!("{:?}", action),
|
||||
&format!("filesystem path '{}'", file_path_str),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft HTTP-Berechtigungen
|
||||
pub async fn check_http_permission(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
method: &str,
|
||||
url: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions = Self::get_permissions(app_state, extension_id).await?;
|
||||
|
||||
let url_parsed = Url::parse(url).map_err(|e| ExtensionError::ValidationError {
|
||||
reason: format!("Invalid URL: {}", e),
|
||||
})?;
|
||||
|
||||
let domain = url_parsed.host_str().unwrap_or("");
|
||||
|
||||
let has_permission = permissions
|
||||
.iter()
|
||||
.filter(|perm| perm.status == PermissionStatus::Granted)
|
||||
.filter(|perm| perm.resource_type == ResourceType::Http)
|
||||
.any(|perm| {
|
||||
let domain_matches = perm.target == "*"
|
||||
|| perm.target == domain
|
||||
|| domain.ends_with(&format!(".{}", perm.target));
|
||||
|
||||
if !domain_matches {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(PermissionConstraints::Http(constraints)) = &perm.constraints {
|
||||
if let Some(methods) = &constraints.methods {
|
||||
if !methods.iter().any(|m| m.eq_ignore_ascii_case(method)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
method,
|
||||
&format!("HTTP request to '{}'", url),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft Shell-Berechtigungen
|
||||
pub async fn check_shell_permission(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
command: &str,
|
||||
args: &[String],
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions = Self::get_permissions(app_state, extension_id).await?;
|
||||
|
||||
let has_permission = permissions
|
||||
.iter()
|
||||
.filter(|perm| perm.status == PermissionStatus::Granted)
|
||||
.filter(|perm| perm.resource_type == ResourceType::Shell)
|
||||
.any(|perm| {
|
||||
if perm.target != command && perm.target != "*" {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(PermissionConstraints::Shell(constraints)) = &perm.constraints {
|
||||
if let Some(allowed_subcommands) = &constraints.allowed_subcommands {
|
||||
if !args.is_empty() {
|
||||
if !allowed_subcommands.contains(&args[0])
|
||||
&& !allowed_subcommands.contains(&"*".to_string())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(forbidden) = &constraints.forbidden_args {
|
||||
if args.iter().any(|arg| forbidden.contains(arg)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(allowed_flags) = &constraints.allowed_flags {
|
||||
let user_flags: Vec<_> =
|
||||
args.iter().filter(|arg| arg.starts_with('-')).collect();
|
||||
|
||||
for flag in user_flags {
|
||||
if !allowed_flags.contains(flag)
|
||||
&& !allowed_flags.contains(&"*".to_string())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
"execute",
|
||||
&format!("shell command '{}' with args {:?}", command, args),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper-Methoden - müssen DatabaseError statt ExtensionError zurückgeben
|
||||
fn parse_resource_type(s: &str) -> Result<ResourceType, DatabaseError> {
|
||||
match s {
|
||||
"fs" => Ok(ResourceType::Fs),
|
||||
"http" => Ok(ResourceType::Http),
|
||||
"db" => Ok(ResourceType::Db),
|
||||
"shell" => Ok(ResourceType::Shell),
|
||||
_ => Err(DatabaseError::SerializationError {
|
||||
reason: format!("Unknown resource type: {}", s),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_action(s: &str) -> Result<Action, DatabaseError> {
|
||||
match s {
|
||||
"read" => Ok(Action::Read),
|
||||
"write" => Ok(Action::Write),
|
||||
_ => Err(DatabaseError::SerializationError {
|
||||
reason: format!("Unknown action: {}", s),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_constraints(
|
||||
resource_type: &ResourceType,
|
||||
json: &str,
|
||||
) -> Result<PermissionConstraints, DatabaseError> {
|
||||
match resource_type {
|
||||
ResourceType::Db => {
|
||||
let constraints: DbConstraints = serde_json::from_str(json)
|
||||
.map_err(|e| DatabaseError::SerializationError {
|
||||
reason: format!("Failed to parse DB constraints: {}", e),
|
||||
})?;
|
||||
Ok(PermissionConstraints::Database(constraints))
|
||||
}
|
||||
ResourceType::Fs => {
|
||||
let constraints: FsConstraints = serde_json::from_str(json)
|
||||
.map_err(|e| DatabaseError::SerializationError {
|
||||
reason: format!("Failed to parse FS constraints: {}", e),
|
||||
})?;
|
||||
Ok(PermissionConstraints::Filesystem(constraints))
|
||||
}
|
||||
ResourceType::Http => {
|
||||
let constraints: HttpConstraints = serde_json::from_str(json)
|
||||
.map_err(|e| DatabaseError::SerializationError {
|
||||
reason: format!("Failed to parse HTTP constraints: {}", e),
|
||||
})?;
|
||||
Ok(PermissionConstraints::Http(constraints))
|
||||
}
|
||||
ResourceType::Shell => {
|
||||
let constraints: ShellConstraints = serde_json::from_str(json)
|
||||
.map_err(|e| DatabaseError::SerializationError {
|
||||
reason: format!("Failed to parse Shell constraints: {}", e),
|
||||
})?;
|
||||
Ok(PermissionConstraints::Shell(constraints))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_path_pattern(pattern: &str, path: &str) -> bool {
|
||||
if pattern.ends_with("/*") {
|
||||
let prefix = &pattern[..pattern.len() - 2];
|
||||
return path.starts_with(prefix);
|
||||
}
|
||||
|
||||
if pattern.starts_with("*.") {
|
||||
let suffix = &pattern[1..];
|
||||
return path.ends_with(suffix);
|
||||
}
|
||||
|
||||
if pattern.contains('*') {
|
||||
let parts: Vec<&str> = pattern.split('*').collect();
|
||||
if parts.len() == 2 {
|
||||
return path.starts_with(parts[0]) && path.ends_with(parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
pattern == path || pattern == "*"
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Convenience-Funktionen für die verschiedenen Subsysteme
|
||||
impl PermissionManager {
|
||||
// Convenience-Methoden
|
||||
pub async fn can_read_file(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
file_path: &Path,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_filesystem_permission(app_state, extension_id, Action::Read, file_path).await
|
||||
}
|
||||
|
||||
pub async fn can_write_file(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
file_path: &Path,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_filesystem_permission(app_state, extension_id, Action::Write, file_path).await
|
||||
}
|
||||
|
||||
pub async fn can_read_table(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
table_name: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_database_permission(app_state, extension_id, Action::Read, table_name).await
|
||||
}
|
||||
|
||||
pub async fn can_write_table(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
table_name: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_database_permission(app_state, extension_id, Action::Write, table_name).await
|
||||
}
|
||||
|
||||
pub async fn can_http_get(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
url: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_http_permission(app_state, extension_id, "GET", url).await
|
||||
}
|
||||
|
||||
pub async fn can_http_post(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
url: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_http_permission(app_state, extension_id, "POST", url).await
|
||||
}
|
||||
|
||||
pub async fn can_execute_command(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
command: &str,
|
||||
args: &[String],
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_shell_permission(app_state, extension_id, command, args).await
|
||||
}
|
||||
|
||||
pub async fn grant_permission(
|
||||
app_state: &State<'_, AppState>,
|
||||
permission_id: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::update_permission_status(app_state, permission_id, PermissionStatus::Granted).await
|
||||
}
|
||||
|
||||
pub async fn deny_permission(
|
||||
app_state: &State<'_, AppState>,
|
||||
permission_id: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::update_permission_status(app_state, permission_id, PermissionStatus::Denied).await
|
||||
}
|
||||
|
||||
pub async fn ask_permission(
|
||||
app_state: &State<'_, AppState>,
|
||||
permission_id: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::update_permission_status(app_state, permission_id, PermissionStatus::Ask).await
|
||||
}
|
||||
|
||||
pub async fn get_ask_permissions(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
) -> Result<Vec<ExtensionPermission>, ExtensionError> {
|
||||
let all_permissions = Self::get_permissions(app_state, extension_id).await?;
|
||||
Ok(all_permissions
|
||||
.into_iter()
|
||||
.filter(|perm| perm.status == PermissionStatus::Ask)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
3
src-tauri/src/extension/permissions/mod.rs
Normal file
3
src-tauri/src/extension/permissions/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod manager;
|
||||
pub mod types;
|
||||
pub mod validator;
|
||||
156
src-tauri/src/extension/permissions/types.rs
Normal file
156
src-tauri/src/extension/permissions/types.rs
Normal file
@ -0,0 +1,156 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::database::error::DatabaseError;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionPermission {
|
||||
pub id: String,
|
||||
pub extension_id: String,
|
||||
pub resource_type: ResourceType,
|
||||
pub action: Action,
|
||||
pub target: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub constraints: Option<PermissionConstraints>,
|
||||
pub status: PermissionStatus,
|
||||
|
||||
// CRDT Felder
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub haex_tombstone: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub haex_timestamp: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ResourceType {
|
||||
Fs,
|
||||
Http,
|
||||
Db,
|
||||
Shell,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Action {
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PermissionStatus {
|
||||
Ask,
|
||||
Granted,
|
||||
Denied,
|
||||
}
|
||||
|
||||
impl PermissionStatus {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
PermissionStatus::Ask => "ask",
|
||||
PermissionStatus::Granted => "granted",
|
||||
PermissionStatus::Denied => "denied",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Result<Self, DatabaseError> {
|
||||
match s {
|
||||
"ask" => Ok(PermissionStatus::Ask),
|
||||
"granted" => Ok(PermissionStatus::Granted),
|
||||
"denied" => Ok(PermissionStatus::Denied),
|
||||
_ => Err(DatabaseError::SerializationError {
|
||||
reason: format!("Unknown permission status: {}", s),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum PermissionConstraints {
|
||||
Database(DbConstraints),
|
||||
Filesystem(FsConstraints),
|
||||
Http(HttpConstraints),
|
||||
Shell(ShellConstraints),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DbConstraints {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub where_clause: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub columns: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct FsConstraints {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_file_size: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allowed_extensions: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub recursive: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct HttpConstraints {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub methods: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rate_limit: Option<RateLimit>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct RateLimit {
|
||||
pub requests: u32,
|
||||
pub per_minutes: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ShellConstraints {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allowed_subcommands: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allowed_flags: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub forbidden_args: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// Wenn du weiterhin gruppierte Permissions brauchst:
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct EditablePermissions {
|
||||
pub permissions: Vec<ExtensionPermission>,
|
||||
}
|
||||
|
||||
// Oder gruppiert nach Typ:
|
||||
impl EditablePermissions {
|
||||
pub fn database_permissions(&self) -> Vec<&ExtensionPermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.filter(|p| p.resource_type == ResourceType::Db)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn filesystem_permissions(&self) -> Vec<&ExtensionPermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.filter(|p| p.resource_type == ResourceType::Fs)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn http_permissions(&self) -> Vec<&ExtensionPermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.filter(|p| p.resource_type == ResourceType::Http)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn shell_permissions(&self) -> Vec<&ExtensionPermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.filter(|p| p.resource_type == ResourceType::Shell)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
201
src-tauri/src/extension/permissions/validator.rs
Normal file
201
src-tauri/src/extension/permissions/validator.rs
Normal file
@ -0,0 +1,201 @@
|
||||
// src-tauri/src/extension/permissions/validator.rs
|
||||
|
||||
use crate::database::core::{extract_table_names_from_sql, parse_single_statement};
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::manager::PermissionManager;
|
||||
use crate::extension::permissions::types::Action;
|
||||
use crate::AppState;
|
||||
use sqlparser::ast::{Statement, TableFactor, TableObject};
|
||||
use tauri::State;
|
||||
|
||||
pub struct SqlPermissionValidator;
|
||||
|
||||
impl SqlPermissionValidator {
|
||||
/// Validiert ein SQL-Statement gegen die Permissions einer Extension
|
||||
pub async fn validate_sql(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
sql: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
|
||||
reason: e.to_string(),
|
||||
sql: sql.to_string(),
|
||||
})?;
|
||||
|
||||
match &statement {
|
||||
Statement::Query(_) => {
|
||||
Self::validate_read_statement(app_state, extension_id, sql).await
|
||||
}
|
||||
Statement::Insert(_) | Statement::Update { .. } | Statement::Delete(_) => {
|
||||
Self::validate_write_statement(app_state, extension_id, &statement).await
|
||||
}
|
||||
Statement::CreateTable(_) => {
|
||||
Self::validate_create_statement(app_state, extension_id, &statement).await
|
||||
}
|
||||
Statement::AlterTable { .. } | Statement::Drop { .. } => {
|
||||
Self::validate_schema_statement(app_state, extension_id, &statement).await
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: format!("Statement type not allowed: {}", sql),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validiert READ-Operationen (SELECT)
|
||||
async fn validate_read_statement(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
sql: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let tables = extract_table_names_from_sql(sql)?;
|
||||
|
||||
for table_name in tables {
|
||||
PermissionManager::check_database_permission(
|
||||
app_state,
|
||||
extension_id,
|
||||
Action::Read,
|
||||
&table_name,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validiert WRITE-Operationen (INSERT, UPDATE, DELETE)
|
||||
async fn validate_write_statement(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
statement: &Statement,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let table_names = Self::extract_table_names_from_statement(statement)?;
|
||||
|
||||
for table_name in table_names {
|
||||
PermissionManager::check_database_permission(
|
||||
app_state,
|
||||
extension_id,
|
||||
Action::Write,
|
||||
&table_name,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validiert CREATE TABLE
|
||||
async fn validate_create_statement(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
statement: &Statement,
|
||||
) -> Result<(), ExtensionError> {
|
||||
if let Statement::CreateTable(create_table) = statement {
|
||||
let table_name = create_table.name.to_string();
|
||||
|
||||
// Prüfe ob Extension überhaupt CREATE-Rechte hat (z.B. auf "*")
|
||||
PermissionManager::check_database_permission(
|
||||
app_state,
|
||||
extension_id,
|
||||
Action::Write,
|
||||
&table_name,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validiert Schema-Änderungen (ALTER, DROP)
|
||||
async fn validate_schema_statement(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
statement: &Statement,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let table_names = Self::extract_table_names_from_statement(statement)?;
|
||||
|
||||
for table_name in table_names {
|
||||
// ALTER/DROP benötigen WRITE-Rechte
|
||||
PermissionManager::check_database_permission(
|
||||
app_state,
|
||||
extension_id,
|
||||
Action::Write,
|
||||
&table_name,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extrahiert alle Tabellennamen aus einem Statement
|
||||
fn extract_table_names_from_statement(
|
||||
statement: &Statement,
|
||||
) -> Result<Vec<String>, ExtensionError> {
|
||||
match statement {
|
||||
Statement::Insert(insert) => Ok(vec![Self::extract_table_name_from_insert(insert)?]),
|
||||
Statement::Update { table, .. } => {
|
||||
Ok(vec![Self::extract_table_name_from_table_factor(
|
||||
&table.relation,
|
||||
)?])
|
||||
}
|
||||
Statement::Delete(delete) => Ok(vec![Self::extract_table_name_from_delete(delete)?]),
|
||||
Statement::CreateTable(create_table) => Ok(vec![create_table.name.to_string()]),
|
||||
Statement::AlterTable { name, .. } => Ok(vec![name.to_string()]),
|
||||
Statement::Drop { names, .. } => {
|
||||
Ok(names.iter().map(|name| name.to_string()).collect())
|
||||
}
|
||||
_ => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrahiert Tabellenname aus INSERT
|
||||
fn extract_table_name_from_insert(
|
||||
insert: &sqlparser::ast::Insert,
|
||||
) -> Result<String, ExtensionError> {
|
||||
match &insert.table {
|
||||
TableObject::TableName(name) => Ok(name.to_string()),
|
||||
_ => Err(DatabaseError::NoTableError {
|
||||
sql: insert.to_string(),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrahiert Tabellenname aus TableFactor
|
||||
fn extract_table_name_from_table_factor(
|
||||
table_factor: &TableFactor,
|
||||
) -> Result<String, ExtensionError> {
|
||||
match table_factor {
|
||||
TableFactor::Table { name, .. } => Ok(name.to_string()),
|
||||
_ => Err(DatabaseError::StatementError {
|
||||
reason: "Complex table references not supported".to_string(),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrahiert Tabellenname aus DELETE
|
||||
fn extract_table_name_from_delete(
|
||||
delete: &sqlparser::ast::Delete,
|
||||
) -> Result<String, ExtensionError> {
|
||||
use sqlparser::ast::FromTable;
|
||||
|
||||
let table_name = match &delete.from {
|
||||
FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => {
|
||||
if !tables.is_empty() {
|
||||
Self::extract_table_name_from_table_factor(&tables[0].relation)?
|
||||
} else if !delete.tables.is_empty() {
|
||||
delete.tables[0].to_string()
|
||||
} else {
|
||||
return Err(DatabaseError::NoTableError {
|
||||
sql: delete.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(table_name)
|
||||
}
|
||||
}
|
||||
@ -1,28 +1,22 @@
|
||||
//mod browser;
|
||||
//mod android_storage;
|
||||
mod crdt;
|
||||
mod database;
|
||||
mod extension;
|
||||
|
||||
//mod models;
|
||||
use crate::{
|
||||
crdt::hlc::HlcService,
|
||||
database::DbConnection,
|
||||
extension::core::{ExtensionManager, ExtensionState},
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::Manager;
|
||||
|
||||
pub mod table_names {
|
||||
include!(concat!(env!("OUT_DIR"), "/tableNames.rs"));
|
||||
}
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{crdt::hlc::HlcService, database::DbConnection, extension::core::ExtensionState};
|
||||
|
||||
/* use crate::{
|
||||
crdt::hlc::HlcService,
|
||||
database::{AppState, DbConnection},
|
||||
extension::core::ExtensionState,
|
||||
}; */
|
||||
|
||||
pub struct AppState {
|
||||
pub db: DbConnection,
|
||||
pub hlc: Mutex<HlcService>, // Kein Arc hier nötig, da der ganze AppState von Tauri in einem Arc verwaltet wird.
|
||||
pub hlc: Mutex<HlcService>,
|
||||
pub extension_manager: ExtensionManager,
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
@ -31,26 +25,27 @@ pub fn run() {
|
||||
|
||||
tauri::Builder::default()
|
||||
.register_uri_scheme_protocol(protocol_name, move |context, request| {
|
||||
match extension::core::extension_protocol_handler(&context, &request) {
|
||||
Ok(response) => response, // Wenn der Handler Ok ist, gib die Response direkt zurück
|
||||
// Hole den AppState aus dem Context
|
||||
let app_handle = context.app_handle();
|
||||
let state = app_handle.state::<AppState>();
|
||||
|
||||
// Rufe den Handler mit allen benötigten Parametern auf
|
||||
match extension::core::extension_protocol_handler(state, &app_handle, &request) {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
// Wenn der Handler einen Fehler zurückgibt, logge ihn und erstelle eine Fehler-Response
|
||||
eprintln!(
|
||||
"Fehler im Custom Protocol Handler für URI '{}': {}",
|
||||
request.uri(),
|
||||
e
|
||||
);
|
||||
// Erstelle eine HTTP 500 Fehler-Response
|
||||
// Du kannst hier auch spezifischere Fehler-Responses bauen, falls gewünscht.
|
||||
tauri::http::Response::builder()
|
||||
.status(500)
|
||||
.header("Content-Type", "text/plain") // Optional, aber gut für Klarheit
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(Vec::from(format!(
|
||||
"Interner Serverfehler im Protokollhandler: {}",
|
||||
e
|
||||
)))
|
||||
.unwrap_or_else(|build_err| {
|
||||
// Fallback, falls selbst das Erstellen der Fehler-Response fehlschlägt
|
||||
eprintln!("Konnte Fehler-Response nicht erstellen: {}", build_err);
|
||||
tauri::http::Response::builder()
|
||||
.status(500)
|
||||
@ -60,11 +55,10 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
})
|
||||
/* .manage(database::DbConnection(Arc::new(Mutex::new(None))))
|
||||
.manage(crdt::hlc::HlcService::new()) */
|
||||
.manage(AppState {
|
||||
db: DbConnection(Arc::new(Mutex::new(None))),
|
||||
hlc: Mutex::new(HlcService::new()), // Starte mit einem uninitialisierten HLC
|
||||
hlc: Mutex::new(HlcService::new()),
|
||||
extension_manager: ExtensionManager::new(),
|
||||
})
|
||||
.manage(ExtensionState::default())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
@ -75,7 +69,6 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_persisted_scope::init())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
//.plugin(tauri_plugin_android_fs::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
database::create_encrypted_database,
|
||||
database::delete_vault,
|
||||
@ -86,36 +79,13 @@ pub fn run() {
|
||||
database::vault_exists,
|
||||
extension::database::extension_sql_execute,
|
||||
extension::database::extension_sql_select,
|
||||
//database::update_hlc_from_remote,
|
||||
/* extension::copy_directory,
|
||||
extension::database::extension_sql_select, */
|
||||
extension::get_all_extensions,
|
||||
extension::get_extension_info,
|
||||
extension::install_extension_with_permissions,
|
||||
extension::is_extension_installed,
|
||||
extension::preview_extension,
|
||||
extension::remove_extension,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
/* fn extension_protocol_handler(
|
||||
app_handle: &tauri::AppHandle, // Beachten Sie die Signaturänderung in neueren Tauri-Versionen
|
||||
request: &tauri::http::Request<Vec<u8>>,
|
||||
) -> Result<tauri::http::Response<Vec<u8>>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let uri_str = request.uri().to_string();
|
||||
let parsed_url = match Url::parse(&uri_str) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
eprintln!("Fehler beim Parsen der URL '{}': {}", uri_str, e);
|
||||
return Ok(tauri::http::ResponseBuilder::new().status(400).body(Vec::from("Ungültige URL"))?);
|
||||
}
|
||||
};
|
||||
|
||||
let plugin_id = parsed_url.host_str().ok_or_else(|| "Fehlende Plugin-ID in der URL".to_string())?;
|
||||
let path_segments: Vec<&str> = parsed_url.path_segments().ok_or_else(|| "URL hat keinen Pfad".to_string())?.collect();
|
||||
|
||||
if path_segments.len() < 2 {
|
||||
eprintln!("Unvollständiger Pfad in URL: {}", uri_str);
|
||||
return Ok(tauri::http::Response::new().status(400).body(Vec::from("Unvollständiger Pfad"))?);
|
||||
}
|
||||
|
||||
let version = path_segments;
|
||||
let file_path = path_segments[1..].join("/");
|
||||
Ok(tauri::http::Response::builder()::new().status(404).body(Vec::new())?)
|
||||
} */
|
||||
|
||||
@ -4,20 +4,9 @@
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div class="absolute top-2 right-2">
|
||||
<UiDropdown class="btn btn-sm btn-text btn-circle">
|
||||
<template #activator>
|
||||
<Icon name="mdi:dots-vertical" />
|
||||
</template>
|
||||
|
||||
<template #items>
|
||||
<UiButton
|
||||
class="btn-error btn-outline btn-sm"
|
||||
@click="showRemoveDialog = true"
|
||||
>
|
||||
<Icon name="mdi:trash" /> {{ t('remove') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiDropdown>
|
||||
<UDropdownMenu>
|
||||
<UiButton icon="mdi:dots-vertical" />
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
|
||||
<div class="card-header">
|
||||
@ -44,6 +33,7 @@
|
||||
<div class="card-actions" v-if="$slots.action">
|
||||
<slot name="action" />
|
||||
</div> -->
|
||||
hier klicken
|
||||
<div
|
||||
class="size-20 absolute bottom-2 right-2"
|
||||
v-html="icon"
|
||||
|
||||
@ -1,71 +1,107 @@
|
||||
<template>
|
||||
<UiAccordion v-if="database?.read?.length">
|
||||
<UAccordion v-if="database?.read?.length">
|
||||
<template #title>
|
||||
<h3>{{ t("permission.read") }}</h3>
|
||||
<h3>{{ t('permission.read') }}</h3>
|
||||
</template>
|
||||
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="read in database?.read" class="flex items-center justify-between px-4 py-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<input :id="Object.keys(read).at(0)" type="checkbox" class="checkbox" :checked="Object.values(read).at(0)" >
|
||||
<label class="label-text text-base" :for="Object.keys(read).at(0)">{{ Object.keys(read).at(0) }}</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UiAccordion>
|
||||
|
||||
<UiAccordion v-if="database?.write?.length">
|
||||
<template #title>
|
||||
<h3>{{ t("permission.write") }}</h3>
|
||||
</template>
|
||||
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="write in database?.write" class="flex items-center justify-between px-4 py-0.5">
|
||||
<li
|
||||
v-for="read in database?.read"
|
||||
class="flex items-center justify-between px-4 py-1"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
:id="Object.keys(write).at(0)" type="checkbox" class="checkbox"
|
||||
:checked="Object.values(write).at(0)" >
|
||||
<label class="label-text text-base" :for="Object.keys(write).at(0)">{{ Object.keys(write).at(0) }}</label>
|
||||
:id="Object.keys(read).at(0)"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="Object.values(read).at(0)"
|
||||
/>
|
||||
<label
|
||||
class="label-text text-base"
|
||||
:for="Object.keys(read).at(0)"
|
||||
>{{ Object.keys(read).at(0) }}</label
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UiAccordion>
|
||||
</UAccordion>
|
||||
|
||||
<UiAccordion v-if="database?.create?.length">
|
||||
<UAccordion v-if="database?.write?.length">
|
||||
<template #title>
|
||||
<h3>{{ t("permission.create") }}</h3>
|
||||
<h3>{{ t('permission.write') }}</h3>
|
||||
</template>
|
||||
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="create in database?.create" class="flex items-center justify-between px-4 py-0.5">
|
||||
<li
|
||||
v-for="write in database?.write"
|
||||
class="flex items-center justify-between px-4 py-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
:id="Object.keys(create).at(0)" type="checkbox" class="checkbox"
|
||||
:checked="Object.values(create).at(0)" >
|
||||
<label class="label-text text-base" :for="Object.keys(create).at(0)">{{ Object.keys(create).at(0) }}</label>
|
||||
:id="Object.keys(write).at(0)"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="Object.values(write).at(0)"
|
||||
/>
|
||||
<label
|
||||
class="label-text text-base"
|
||||
:for="Object.keys(write).at(0)"
|
||||
>{{ Object.keys(write).at(0) }}</label
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UiAccordion>
|
||||
</UAccordion>
|
||||
|
||||
<UAccordion v-if="database?.create?.length">
|
||||
<template #title>
|
||||
<h3>{{ t('permission.create') }}</h3>
|
||||
</template>
|
||||
|
||||
<ul class="space-y-0.5">
|
||||
<li
|
||||
v-for="create in database?.create"
|
||||
class="flex items-center justify-between px-4 py-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
:id="Object.keys(create).at(0)"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="Object.values(create).at(0)"
|
||||
/>
|
||||
<label
|
||||
class="label-text text-base"
|
||||
:for="Object.keys(create).at(0)"
|
||||
>{{ Object.keys(create).at(0) }}</label
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UAccordion>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
defineProps<{ database?: { read?: Record<string, boolean>[], write?: Record<string, boolean>[], create?: Record<string, boolean>[] } }>();
|
||||
const { t } = useI18n();
|
||||
defineProps<{
|
||||
database?: {
|
||||
read?: Record<string, boolean>[]
|
||||
write?: Record<string, boolean>[]
|
||||
create?: Record<string, boolean>[]
|
||||
}
|
||||
}>()
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
permission:
|
||||
read: Lesen
|
||||
write: Schreiben
|
||||
create: Erstellen
|
||||
de:
|
||||
permission:
|
||||
read: Lesen
|
||||
write: Schreiben
|
||||
create: Erstellen
|
||||
|
||||
en:
|
||||
permission:
|
||||
read: Read
|
||||
write: Write
|
||||
create: Create
|
||||
en:
|
||||
permission:
|
||||
read: Read
|
||||
write: Write
|
||||
create: Create
|
||||
</i18n>
|
||||
|
||||
@ -1,38 +1,56 @@
|
||||
<template>
|
||||
<UiAccordion v-if="filesystem?.read?.length">
|
||||
<UAccordion v-if="filesystem?.read?.length">
|
||||
<template #title>
|
||||
<h3>{{ t('permission.read') }}</h3>
|
||||
</template>
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="read in filesystem?.read" class="flex items-center justify-between px-4 py-0.5">
|
||||
<li
|
||||
v-for="read in filesystem?.read"
|
||||
class="flex items-center justify-between px-4 py-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<input :id="Object.keys(read).at(0)" type="checkbox" class="checkbox" :checked="Object.values(read).at(0)" >
|
||||
<label class="label-text text-base" :for="Object.keys(read).at(0)">{{
|
||||
Object.keys(read).at(0)
|
||||
}}</label>
|
||||
<input
|
||||
:id="Object.keys(read).at(0)"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="Object.values(read).at(0)"
|
||||
/>
|
||||
<label
|
||||
class="label-text text-base"
|
||||
:for="Object.keys(read).at(0)"
|
||||
>{{ Object.keys(read).at(0) }}</label
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UiAccordion>
|
||||
</UAccordion>
|
||||
|
||||
<UiAccordion v-if="filesystem?.write?.length">
|
||||
<UAccordion v-if="filesystem?.write?.length">
|
||||
<template #title>
|
||||
<h3>{{ t('permission.write') }}</h3>
|
||||
</template>
|
||||
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="write in filesystem?.write" class="flex items-center justify-between px-4 py-0.5">
|
||||
<li
|
||||
v-for="write in filesystem?.write"
|
||||
class="flex items-center justify-between px-4 py-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
:id="Object.keys(write).at(0)" type="checkbox" class="checkbox"
|
||||
:checked="Object.values(write).at(0)" >
|
||||
<label class="label-text text-base" :for="Object.keys(write).at(0)">{{
|
||||
Object.keys(write).at(0)
|
||||
}}</label>
|
||||
:id="Object.keys(write).at(0)"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="Object.values(write).at(0)"
|
||||
/>
|
||||
<label
|
||||
class="label-text text-base"
|
||||
:for="Object.keys(write).at(0)"
|
||||
>{{ Object.keys(write).at(0) }}</label
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UiAccordion>
|
||||
</UAccordion>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -46,13 +64,13 @@ const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
permission:
|
||||
read: Lesen
|
||||
write: Schreiben
|
||||
de:
|
||||
permission:
|
||||
read: Lesen
|
||||
write: Schreiben
|
||||
|
||||
en:
|
||||
permission:
|
||||
read: Read
|
||||
write: Write
|
||||
en:
|
||||
permission:
|
||||
read: Read
|
||||
write: Write
|
||||
</i18n>
|
||||
|
||||
@ -1,33 +1,43 @@
|
||||
<template>
|
||||
<UiAccordion>
|
||||
<UAccordion>
|
||||
<template #title>
|
||||
<h3>{{ t("http.access") }}</h3>
|
||||
<h3>{{ t('http.access') }}</h3>
|
||||
</template>
|
||||
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="access in http" class="flex items-center justify-between px-4 py-0.5">
|
||||
<li
|
||||
v-for="access in http"
|
||||
class="flex items-center justify-between px-4 py-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
:id="Object.keys(access).at(0)" type="checkbox" class="checkbox"
|
||||
:checked="Object.values(access).at(0)" >
|
||||
<label class="label-text text-base" :for="Object.keys(access).at(0)">{{ Object.keys(access).at(0) }}</label>
|
||||
:id="Object.keys(access).at(0)"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="Object.values(access).at(0)"
|
||||
/>
|
||||
<label
|
||||
class="label-text text-base"
|
||||
:for="Object.keys(access).at(0)"
|
||||
>{{ Object.keys(access).at(0) }}</label
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UiAccordion>
|
||||
</UAccordion>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ http?: Record<string, boolean>[] }>();
|
||||
const { t } = useI18n();
|
||||
defineProps<{ http?: Record<string, boolean>[] }>()
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
http:
|
||||
access: Internet Zugriff
|
||||
de:
|
||||
http:
|
||||
access: Internet Zugriff
|
||||
|
||||
en:
|
||||
http:
|
||||
access: Internet Access
|
||||
en:
|
||||
http:
|
||||
access: Internet Access
|
||||
</i18n>
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
/**
|
||||
* Broadcasts context changes to all active extensions
|
||||
*/
|
||||
// composables/extensionContextBroadcast.ts
|
||||
export const useExtensionContextBroadcast = () => {
|
||||
const extensionIframes = ref<HTMLIFrameElement[]>([])
|
||||
// Globaler State für alle aktiven IFrames
|
||||
const extensionIframes = useState<Set<HTMLIFrameElement>>(
|
||||
'extension-iframes',
|
||||
() => new Set(),
|
||||
)
|
||||
|
||||
const registerExtensionIframe = (iframe: HTMLIFrameElement) => {
|
||||
extensionIframes.value.push(iframe)
|
||||
extensionIframes.value.add(iframe)
|
||||
}
|
||||
|
||||
const unregisterExtensionIframe = (iframe: HTMLIFrameElement) => {
|
||||
extensionIframes.value = extensionIframes.value.filter((f) => f !== iframe)
|
||||
extensionIframes.value.delete(iframe)
|
||||
}
|
||||
|
||||
const broadcastContextChange = (context: {
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import type { IHaexHubExtensionLink } from '~/types/haexhub'
|
||||
// composables/extensionMessageHandler.ts
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||
|
||||
interface ExtensionRequest {
|
||||
id: string
|
||||
@ -7,119 +9,124 @@ interface ExtensionRequest {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
interface ExtensionResponse {
|
||||
id: string
|
||||
result?: unknown
|
||||
error?: {
|
||||
code: string
|
||||
message: string
|
||||
details?: unknown
|
||||
}
|
||||
}
|
||||
// Globaler Handler - nur einmal registriert
|
||||
let globalHandlerRegistered = false
|
||||
const iframeRegistry = new Map<HTMLIFrameElement, IHaexHubExtension>()
|
||||
|
||||
export const useExtensionMessageHandler = (
|
||||
iframeRef: Ref<HTMLIFrameElement | undefined | null>,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
) => {
|
||||
const handleMessage = async (event: MessageEvent) => {
|
||||
// Security: Only accept messages from our iframe
|
||||
if (!iframeRef.value || event.source !== iframeRef.value.contentWindow) {
|
||||
return
|
||||
const registerGlobalMessageHandler = () => {
|
||||
if (globalHandlerRegistered) return
|
||||
|
||||
window.addEventListener('message', async (event: MessageEvent) => {
|
||||
// Finde die Extension für dieses IFrame
|
||||
let extension: IHaexHubExtension | undefined
|
||||
let sourceIframe: HTMLIFrameElement | undefined
|
||||
|
||||
for (const [iframe, ext] of iframeRegistry.entries()) {
|
||||
if (event.source === iframe.contentWindow) {
|
||||
extension = ext
|
||||
sourceIframe = iframe
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!extension || !sourceIframe) {
|
||||
return // Message ist nicht von einem registrierten IFrame
|
||||
}
|
||||
|
||||
const request = event.data as ExtensionRequest
|
||||
|
||||
// Validate request structure
|
||||
if (!request.id || !request.method) {
|
||||
console.error('Invalid extension request:', request)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[HaexHub] Extension request:', request.method, request.params)
|
||||
console.log(
|
||||
`[HaexHub] ${extension.name} request:`,
|
||||
request.method,
|
||||
request.params,
|
||||
)
|
||||
|
||||
try {
|
||||
let result: unknown
|
||||
|
||||
// Route request to appropriate handler
|
||||
if (request.method.startsWith('extension.')) {
|
||||
result = await handleExtensionMethod(request, extension)
|
||||
result = await handleExtensionMethodAsync(request, extension)
|
||||
} else if (request.method.startsWith('db.')) {
|
||||
result = await handleDatabaseMethod(request, extension)
|
||||
result = await handleDatabaseMethodAsync(request, extension)
|
||||
} else if (request.method.startsWith('fs.')) {
|
||||
result = await handleFilesystemMethodAsync(request, extension)
|
||||
} else if (request.method.startsWith('http.')) {
|
||||
result = await handleHttpMethodAsync(request, extension)
|
||||
} else if (request.method.startsWith('permissions.')) {
|
||||
result = await handlePermissionsMethod(request, extension)
|
||||
result = await handlePermissionsMethodAsync(request, extension)
|
||||
} else if (request.method.startsWith('context.')) {
|
||||
result = await handleContextMethod(request)
|
||||
} else if (request.method.startsWith('search.')) {
|
||||
result = await handleSearchMethod(request, extension)
|
||||
result = await handleContextMethodAsync(request)
|
||||
} else {
|
||||
throw new Error(`Unknown method: ${request.method}`)
|
||||
}
|
||||
|
||||
// Send success response
|
||||
sendResponse(iframeRef.value, {
|
||||
id: request.id,
|
||||
result,
|
||||
})
|
||||
sourceIframe.contentWindow?.postMessage(
|
||||
{
|
||||
id: request.id,
|
||||
result,
|
||||
},
|
||||
'*',
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('[HaexHub] Extension request error:', error)
|
||||
|
||||
// Send error response
|
||||
sendResponse(iframeRef.value, {
|
||||
id: request.id,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
details: error,
|
||||
sourceIframe.contentWindow?.postMessage(
|
||||
{
|
||||
id: request.id,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
details: error,
|
||||
},
|
||||
},
|
||||
})
|
||||
'*',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const sendResponse = (
|
||||
iframe: HTMLIFrameElement,
|
||||
response: ExtensionResponse,
|
||||
) => {
|
||||
iframe.contentWindow?.postMessage(response, '*')
|
||||
}
|
||||
|
||||
// Register/unregister message listener
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', handleMessage)
|
||||
})
|
||||
|
||||
globalHandlerRegistered = true
|
||||
}
|
||||
|
||||
export const useExtensionMessageHandler = (
|
||||
iframeRef: Ref<HTMLIFrameElement | undefined | null>,
|
||||
extension: ComputedRef<IHaexHubExtension | undefined | null>,
|
||||
) => {
|
||||
// Registriere globalen Handler beim ersten Aufruf
|
||||
registerGlobalMessageHandler()
|
||||
|
||||
// Registriere dieses IFrame
|
||||
watchEffect(() => {
|
||||
if (iframeRef.value && extension.value) {
|
||||
iframeRegistry.set(iframeRef.value, extension.value)
|
||||
}
|
||||
})
|
||||
|
||||
// Cleanup beim Unmount
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
if (iframeRef.value) {
|
||||
iframeRegistry.delete(iframeRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
handleMessage,
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Extension Methods
|
||||
// ==========================================
|
||||
|
||||
async function handleExtensionMethod(
|
||||
async function handleExtensionMethodAsync(
|
||||
request: ExtensionRequest,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
extension: IHaexHubExtension, // Direkter Typ, kein ComputedRef mehr
|
||||
) {
|
||||
switch (request.method) {
|
||||
case 'extension.getInfo':
|
||||
return {
|
||||
keyHash: extension.value?.id || '', // TODO: Real key hash
|
||||
name: extension.value?.name || '',
|
||||
fullId: `${extension.value?.id}/${extension.value?.name}@${extension.value?.version}`,
|
||||
version: extension.value?.version || '',
|
||||
displayName: extension.value?.name,
|
||||
namespace: extension.value?.author,
|
||||
allowedOrigin: window.location.origin, // "tauri://localhost"
|
||||
}
|
||||
|
||||
case 'extensions.getDependencies':
|
||||
// TODO: Implement dependencies from manifest
|
||||
return []
|
||||
|
||||
return await invoke('get_extension_info', {
|
||||
extensionId: extension.id,
|
||||
})
|
||||
default:
|
||||
throw new Error(`Unknown extension method: ${request.method}`)
|
||||
}
|
||||
@ -129,47 +136,41 @@ async function handleExtensionMethod(
|
||||
// Database Methods
|
||||
// ==========================================
|
||||
|
||||
async function handleDatabaseMethod(
|
||||
async function handleDatabaseMethodAsync(
|
||||
request: ExtensionRequest,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
extension: IHaexHubExtension, // Direkter Typ
|
||||
) {
|
||||
const { currentVault } = useVaultStore()
|
||||
if (!currentVault) {
|
||||
throw new Error('No vault available')
|
||||
const params = request.params as {
|
||||
query?: string
|
||||
params?: unknown[]
|
||||
}
|
||||
|
||||
if (!extension.value) {
|
||||
throw new Error('Extension not found')
|
||||
}
|
||||
|
||||
const params = request.params as { query?: string; params?: unknown[] }
|
||||
|
||||
switch (request.method) {
|
||||
case 'db.query': {
|
||||
// Validate permission
|
||||
await validateDatabaseAccess(extension.value, params.query || '', 'read')
|
||||
|
||||
// Execute query
|
||||
const result = await currentVault.drizzle.execute(params.query || '')
|
||||
const rows = await invoke<unknown[]>('extension_sql_select', {
|
||||
sql: params.query || '',
|
||||
params: params.params || [],
|
||||
extensionId: extension.id,
|
||||
})
|
||||
|
||||
return {
|
||||
rows: result.rows || [],
|
||||
rows,
|
||||
rowsAffected: 0,
|
||||
lastInsertId: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
case 'db.execute': {
|
||||
// Validate permission
|
||||
await validateDatabaseAccess(extension.value, params.query || '', 'write')
|
||||
|
||||
// Execute query
|
||||
const result = await currentVault.drizzle.execute(params.query || '')
|
||||
await invoke<string[]>('extension_sql_execute', {
|
||||
sql: params.query || '',
|
||||
params: params.params || [],
|
||||
extensionId: extension.id,
|
||||
})
|
||||
|
||||
return {
|
||||
rows: [],
|
||||
rowsAffected: result.rowsAffected || 0,
|
||||
lastInsertId: result.lastInsertId,
|
||||
rowsAffected: 1,
|
||||
lastInsertId: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,18 +178,14 @@ async function handleDatabaseMethod(
|
||||
const statements =
|
||||
(request.params as { statements?: string[] }).statements || []
|
||||
|
||||
// Validate all statements
|
||||
for (const stmt of statements) {
|
||||
await validateDatabaseAccess(extension.value, stmt, 'write')
|
||||
await invoke('extension_sql_execute', {
|
||||
sql: stmt,
|
||||
params: [],
|
||||
extensionId: extension.id,
|
||||
})
|
||||
}
|
||||
|
||||
// Execute transaction
|
||||
await currentVault.drizzle.transaction(async (tx) => {
|
||||
for (const stmt of statements) {
|
||||
await tx.execute(stmt)
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
@ -196,125 +193,63 @@ async function handleDatabaseMethod(
|
||||
throw new Error(`Unknown database method: ${request.method}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Permission Validation
|
||||
// Filesystem Methods (TODO)
|
||||
// ==========================================
|
||||
|
||||
async function validateDatabaseAccess(
|
||||
extension: IHaexHubExtensionLink,
|
||||
query: string,
|
||||
operation: 'read' | 'write',
|
||||
): Promise<void> {
|
||||
// Extract table name from query
|
||||
const tableMatch = query.match(/(?:FROM|INTO|UPDATE|TABLE)\s+(\w+)/i)
|
||||
if (!tableMatch) {
|
||||
throw new Error('Could not extract table name from query')
|
||||
}
|
||||
|
||||
const tableName = tableMatch[1]
|
||||
|
||||
// Check if it's the extension's own table
|
||||
const extensionPrefix = `${extension.id}_${extension.name?.replace(/-/g, '_')}_`
|
||||
const isOwnTable = tableName.startsWith(extensionPrefix)
|
||||
|
||||
if (isOwnTable) {
|
||||
// Own tables: always allowed
|
||||
return
|
||||
}
|
||||
|
||||
// External table: Check permissions
|
||||
const hasPermission = await checkDatabasePermission(
|
||||
extension.id,
|
||||
tableName,
|
||||
operation,
|
||||
)
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new Error(`Permission denied: ${operation} access to ${tableName}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDatabasePermission(
|
||||
extensionId: string,
|
||||
tableName: string,
|
||||
operation: 'read' | 'write',
|
||||
): Promise<boolean> {
|
||||
// TODO: Query permissions from database
|
||||
// SELECT * FROM db_extension_permissions
|
||||
// WHERE extension_id = ? AND resource = ? AND operation = ?
|
||||
|
||||
console.warn('TODO: Implement permission check', {
|
||||
extensionId,
|
||||
tableName,
|
||||
operation,
|
||||
})
|
||||
|
||||
// For now: deny by default
|
||||
return false
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Permission Methods
|
||||
// ==========================================
|
||||
|
||||
async function handlePermissionsMethod(
|
||||
async function handleFilesystemMethodAsync(
|
||||
request: ExtensionRequest,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
extension: IHaexHubExtension,
|
||||
) {
|
||||
switch (request.method) {
|
||||
case 'permissions.database.request': {
|
||||
const params = request.params as {
|
||||
resource: string
|
||||
operation: 'read' | 'write'
|
||||
reason?: string
|
||||
}
|
||||
if (!request || !extension) return
|
||||
// TODO: Implementiere Filesystem Commands im Backend
|
||||
throw new Error('Filesystem methods not yet implemented')
|
||||
}
|
||||
|
||||
// TODO: Show user dialog to grant/deny permission
|
||||
console.log('[HaexHub] Permission request:', params)
|
||||
// ==========================================
|
||||
// HTTP Methods (TODO)
|
||||
// ==========================================
|
||||
|
||||
// For now: return ASK
|
||||
return {
|
||||
status: 'ask',
|
||||
permanent: false,
|
||||
}
|
||||
}
|
||||
|
||||
case 'permissions.database.check': {
|
||||
const params = request.params as {
|
||||
resource: string
|
||||
operation: 'read' | 'write'
|
||||
}
|
||||
|
||||
const hasPermission = await checkDatabasePermission(
|
||||
extension.value?.id || '',
|
||||
params.resource,
|
||||
params.operation,
|
||||
)
|
||||
|
||||
return {
|
||||
status: hasPermission ? 'granted' : 'denied',
|
||||
permanent: true,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown permission method: ${request.method}`)
|
||||
async function handleHttpMethodAsync(
|
||||
request: ExtensionRequest,
|
||||
extension: IHaexHubExtension,
|
||||
) {
|
||||
if (!extension || !request) {
|
||||
throw new Error('Extension not found')
|
||||
}
|
||||
|
||||
// TODO: Implementiere HTTP Commands im Backend
|
||||
throw new Error('HTTP methods not yet implemented')
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Permission Methods (TODO)
|
||||
// ==========================================
|
||||
|
||||
async function handlePermissionsMethodAsync(
|
||||
request: ExtensionRequest,
|
||||
extension: IHaexHubExtension,
|
||||
) {
|
||||
if (!extension || !request) {
|
||||
throw new Error('Extension not found')
|
||||
}
|
||||
|
||||
// TODO: Implementiere Permission Request UI
|
||||
throw new Error('Permission methods not yet implemented')
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Context Methods
|
||||
// ==========================================
|
||||
|
||||
async function handleContextMethod(request: ExtensionRequest) {
|
||||
const { theme } = useThemeStore()
|
||||
async function handleContextMethodAsync(request: ExtensionRequest) {
|
||||
const { currentTheme } = storeToRefs(useUiStore())
|
||||
const { locale } = useI18n()
|
||||
|
||||
switch (request.method) {
|
||||
case 'context.get':
|
||||
return {
|
||||
theme: theme.value || 'system',
|
||||
theme: currentTheme.value || 'system',
|
||||
locale: locale.value,
|
||||
platform: detectPlatform(),
|
||||
}
|
||||
@ -330,29 +265,3 @@ function detectPlatform(): 'desktop' | 'mobile' | 'tablet' {
|
||||
if (width < 1024) return 'tablet'
|
||||
return 'desktop'
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Search Methods
|
||||
// ==========================================
|
||||
|
||||
async function handleSearchMethod(
|
||||
request: ExtensionRequest,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
) {
|
||||
switch (request.method) {
|
||||
case 'search.respond': {
|
||||
const params = request.params as {
|
||||
requestId: string
|
||||
results: unknown[]
|
||||
}
|
||||
|
||||
// TODO: Store search results for display
|
||||
console.log('[HaexHub] Search results from extension:', params)
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown search method: ${request.method}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<UPage
|
||||
<div
|
||||
:ui="{
|
||||
root: ['h-full w-full bg-elevated'],
|
||||
root: ['h-full w-full bg-elevated lg:flex'],
|
||||
center: ['h-full w-full'],
|
||||
}"
|
||||
>
|
||||
@ -34,7 +34,7 @@
|
||||
</template>
|
||||
</UiDialogConfirm>
|
||||
</div>
|
||||
</UPage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@ -1,24 +1,65 @@
|
||||
<template>
|
||||
<div class="w-full h-full overflow-scroll">
|
||||
<div
|
||||
v-if="!iFrameSrc"
|
||||
class="flex items-center justify-center h-full"
|
||||
>
|
||||
<p>{{ t('loading') }}</p>
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- Tab Bar -->
|
||||
<div class="flex gap-2 p-2 bg-default overflow-x-auto border-b">
|
||||
<div
|
||||
v-for="tab in tabsStore.sortedTabs"
|
||||
:key="tab.extension.id"
|
||||
:class="[
|
||||
'btn btn-sm gap-2',
|
||||
tabsStore.activeTabId === tab.extension.id
|
||||
? 'btn-primary'
|
||||
: 'btn-ghost',
|
||||
]"
|
||||
@click="tabsStore.setActiveTab(tab.extension.id)"
|
||||
>
|
||||
{{ tab.extension.name }}
|
||||
<button
|
||||
class="ml-1 hover:text-error"
|
||||
@click.stop="tabsStore.closeTab(tab.extension.id)"
|
||||
>
|
||||
<Icon
|
||||
name="mdi:close"
|
||||
size="16"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IFrame Container -->
|
||||
<div class="flex-1 relative overflow-hidden">
|
||||
<div
|
||||
v-for="tab in tabsStore.sortedTabs"
|
||||
:key="tab.extension.id"
|
||||
:style="{ display: tab.isVisible ? 'block' : 'none' }"
|
||||
class="w-full h-full"
|
||||
>
|
||||
<iframe
|
||||
:ref="
|
||||
(el) => registerIFrame(tab.extension.id, el as HTMLIFrameElement)
|
||||
"
|
||||
class="w-full h-full"
|
||||
:src="getExtensionUrl(tab.extension)"
|
||||
sandbox="allow-scripts"
|
||||
allow="autoplay; speaker-selection; encrypted-media;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
v-if="tabsStore.tabCount === 0"
|
||||
class="flex items-center justify-center h-full"
|
||||
>
|
||||
<p>{{ t('loading') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
v-else
|
||||
ref="iFrameRef"
|
||||
class="w-full h-full"
|
||||
:src="iFrameSrc"
|
||||
sandbox="allow-scripts "
|
||||
allow="autoplay; speaker-selection; encrypted-media;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useExtensionMessageHandler } from '~/composables/extensionMessageHandler'
|
||||
import { useExtensionTabsStore } from '~/stores/extensions/tabs'
|
||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||
|
||||
definePageMeta({
|
||||
name: 'haexExtension',
|
||||
@ -26,42 +67,84 @@ definePageMeta({
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const iFrameRef = useTemplateRef('iFrameRef')
|
||||
const tabsStore = useExtensionTabsStore()
|
||||
|
||||
const { extensionEntry: iframeSrc, currentExtension } =
|
||||
storeToRefs(useExtensionsStore())
|
||||
// Extension aus Route öffnen
|
||||
//const extensionId = computed(() => route.params.extensionId as string)
|
||||
|
||||
const iFrameSrc = computed(() =>
|
||||
iframeSrc.value ? `${iframeSrc.value}/index.html` : '',
|
||||
const { currentExtensionId } = storeToRefs(useExtensionsStore())
|
||||
watchEffect(() => {
|
||||
if (currentExtensionId.value) {
|
||||
tabsStore.openTab(currentExtensionId.value)
|
||||
}
|
||||
})
|
||||
|
||||
const messageHandlers = new Map<string, boolean>()
|
||||
|
||||
watch(
|
||||
() => tabsStore.openTabs,
|
||||
(tabs) => {
|
||||
tabs.forEach((tab, id) => {
|
||||
if (tab.iframe && !messageHandlers.has(id)) {
|
||||
const iframeRef = ref(tab.iframe)
|
||||
const extensionRef = computed(() => tab.extension)
|
||||
useExtensionMessageHandler(iframeRef, extensionRef)
|
||||
messageHandlers.set(id, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
useExtensionMessageHandler(iFrameRef, currentExtension)
|
||||
// IFrame Registrierung und Message Handler Setup
|
||||
/* const iframeRefs = new Map<string, HTMLIFrameElement>()
|
||||
const setupMessageHandlers = new Set<string>() */
|
||||
|
||||
const registerIFrame = (extensionId: string, el: HTMLIFrameElement | null) => {
|
||||
if (!el) return
|
||||
tabsStore.registerIFrame(extensionId, el)
|
||||
}
|
||||
// Extension URL generieren
|
||||
const getExtensionUrl = (extension: IHaexHubExtension) => {
|
||||
const info = { id: extension.id, version: extension.version }
|
||||
const jsonString = JSON.stringify(info)
|
||||
const bytes = new TextEncoder().encode(jsonString)
|
||||
const encoded = Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
|
||||
const url = `haex-extension://${encoded}/index.html`
|
||||
console.log('Extension URL:', url, 'for', extension.name)
|
||||
return url
|
||||
}
|
||||
|
||||
// Context Changes an alle Tabs broadcasten
|
||||
const { currentTheme } = storeToRefs(useUiStore())
|
||||
const { locale } = useI18n()
|
||||
|
||||
watch([currentTheme, locale], () => {
|
||||
if (iFrameRef.value?.contentWindow) {
|
||||
iFrameRef.value.contentWindow.postMessage(
|
||||
{
|
||||
type: 'context.changed',
|
||||
data: {
|
||||
context: {
|
||||
theme: currentTheme.value || 'system',
|
||||
locale: locale.value,
|
||||
platform:
|
||||
window.innerWidth < 768
|
||||
? 'mobile'
|
||||
: window.innerWidth < 1024
|
||||
? 'tablet'
|
||||
: 'desktop',
|
||||
},
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
tabsStore.broadcastToAllTabs({
|
||||
type: 'context.changed',
|
||||
data: {
|
||||
context: {
|
||||
theme: currentTheme.value || 'system',
|
||||
locale: locale.value,
|
||||
platform:
|
||||
window.innerWidth < 768
|
||||
? 'mobile'
|
||||
: window.innerWidth < 1024
|
||||
? 'tablet'
|
||||
: 'desktop',
|
||||
},
|
||||
'*',
|
||||
)
|
||||
}
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
})
|
||||
|
||||
// Cleanup beim Verlassen
|
||||
onBeforeUnmount(() => {
|
||||
// Optional: Alle Tabs schließen oder offen lassen
|
||||
// tabsStore.closeAllTabs()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -31,18 +31,17 @@
|
||||
class="size-full md:size-2/3 md:translate-x-1/5 md:translate-y-1/3"
|
||||
/>
|
||||
<div class="fixed top-30 right-10">
|
||||
<UiTooltip :tooltip="t('extension.add')">
|
||||
<UiButton
|
||||
class="btn-square btn-primary btn-xl btn-gradient rotate-45"
|
||||
@click="prepareInstallExtensionAsync"
|
||||
>
|
||||
<Icon
|
||||
name="mdi:plus"
|
||||
size="1.5em"
|
||||
class="rotate-45"
|
||||
/>
|
||||
</UiButton>
|
||||
</UiTooltip>
|
||||
<UiButton
|
||||
class="btn-square btn-primary btn-xl btn-gradient rotate-45"
|
||||
:tooltip="t('extension.add')"
|
||||
@click="prepareInstallExtensionAsync"
|
||||
>
|
||||
<Icon
|
||||
name="mdi:plus"
|
||||
size="1.5em"
|
||||
class="rotate-45"
|
||||
/>
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -93,7 +92,7 @@ const extension = reactive<{
|
||||
path: '',
|
||||
})
|
||||
|
||||
const loadExtensionManifestAsync = async () => {
|
||||
/* const loadExtensionManifestAsync = async () => {
|
||||
try {
|
||||
extension.path = await open({ directory: true, recursive: true })
|
||||
if (!extension.path) return
|
||||
@ -111,7 +110,7 @@ const loadExtensionManifestAsync = async () => {
|
||||
add({ color: 'error', description: JSON.stringify(error) })
|
||||
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
const { add } = useToast()
|
||||
const { addNotificationAsync } = useNotificationStore()
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<UPage>
|
||||
<div class="w-full">
|
||||
<div class="h-screen bg-amber-300 flex-1 flex-wrap">
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
</div>
|
||||
<div class="h-screen bg-teal-300 flex-1">
|
||||
abbbbbbbbbbbbbbbbbbbbb availableThemes:{{ uiStore.availableThemes }}
|
||||
</div>
|
||||
</UPage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@ -1,22 +1,44 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { appDataDir, join } from '@tauri-apps/api/path'
|
||||
import { exists, readDir, readTextFile, remove } from '@tauri-apps/plugin-fs'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
|
||||
import type {
|
||||
IHaexHubExtension,
|
||||
IHaexHubExtensionLink,
|
||||
IHaexHubExtensionManifest,
|
||||
} from '~/types/haexhub'
|
||||
import { haexExtensions } from '~~/src-tauri/database/schemas/vault'
|
||||
|
||||
const manifestFileName = 'manifest.json'
|
||||
const logoFileName = 'icon.svg'
|
||||
interface ExtensionInfoResponse {
|
||||
key_hash: string
|
||||
name: string
|
||||
full_id: string
|
||||
version: string
|
||||
display_name: string | null
|
||||
namespace: string | null
|
||||
allowed_origin: string
|
||||
}
|
||||
|
||||
/* const manifestFileName = 'manifest.json'
|
||||
const logoFileName = 'icon.svg' */
|
||||
|
||||
export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
const availableExtensions = ref<IHaexHubExtensionLink[]>([])
|
||||
const { addNotificationAsync } = useNotificationStore()
|
||||
const availableExtensions = ref<IHaexHubExtension[]>([])
|
||||
const currentRoute = useRouter().currentRoute
|
||||
|
||||
const extensionLinks = computed<ISidebarItem[]>(() =>
|
||||
const currentExtensionId = computed(() =>
|
||||
getSingleRouteParam(currentRoute.value.params.extensionId),
|
||||
)
|
||||
|
||||
const currentExtension = computed(() => {
|
||||
if (!currentExtensionId.value) return null
|
||||
|
||||
return (
|
||||
availableExtensions.value.find(
|
||||
(ext) => ext.id === currentExtensionId.value,
|
||||
) ?? null
|
||||
)
|
||||
})
|
||||
|
||||
/* const { addNotificationAsync } = useNotificationStore() */
|
||||
|
||||
/* const extensionLinks = computed<ISidebarItem[]>(() =>
|
||||
availableExtensions.value
|
||||
.filter((extension) => extension.enabled && extension.installed)
|
||||
.map((extension) => ({
|
||||
@ -26,9 +48,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
tooltip: extension.name ?? '',
|
||||
to: { name: 'haexExtension', params: { extensionId: extension.id } },
|
||||
})),
|
||||
)
|
||||
|
||||
const currentRoute = useRouter().currentRoute
|
||||
) */
|
||||
|
||||
const isActive = (id: string) =>
|
||||
computed(
|
||||
@ -37,32 +57,26 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
currentRoute.value.params.extensionId === id,
|
||||
)
|
||||
|
||||
const currentExtension = computed(() => {
|
||||
console.log('computed currentExtension', currentRoute.value.params)
|
||||
if (currentRoute.value.meta.name !== 'haexExtension') return
|
||||
const extensionEntry = computed(() => {
|
||||
if (!currentExtension.value?.version || !currentExtension.value?.id)
|
||||
return null
|
||||
|
||||
const extensionId = getSingleRouteParam(
|
||||
currentRoute.value.params.extensionId,
|
||||
const encodedInfo = encodeExtensionInfo(
|
||||
currentExtension.value.id,
|
||||
currentExtension.value.version,
|
||||
)
|
||||
console.log('extensionId from param', extensionId)
|
||||
if (!extensionId) return
|
||||
|
||||
const extension = availableExtensions.value.find(
|
||||
(extension) => extension.id === extensionId,
|
||||
)
|
||||
console.log('currentExtension', extension)
|
||||
return extension
|
||||
return `extension://${encodedInfo}`
|
||||
})
|
||||
|
||||
const getExtensionPathAsync = async (
|
||||
/* const getExtensionPathAsync = async (
|
||||
extensionId?: string,
|
||||
version?: string,
|
||||
) => {
|
||||
if (!extensionId || !version) return ''
|
||||
return await join(await appDataDir(), 'extensions', extensionId, version)
|
||||
}
|
||||
} */
|
||||
|
||||
const checkSourceExtensionDirectoryAsync = async (
|
||||
/* const checkSourceExtensionDirectoryAsync = async (
|
||||
extensionDirectory: string,
|
||||
) => {
|
||||
try {
|
||||
@ -82,22 +96,154 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
||||
//throw error //new Error(`Keine Leseberechtigung für Ordner ${extensionDirectory}`);
|
||||
}
|
||||
} */
|
||||
|
||||
const loadExtensionsAsync = async () => {
|
||||
try {
|
||||
const extensions =
|
||||
await invoke<ExtensionInfoResponse[]>('get_all_extensions')
|
||||
|
||||
availableExtensions.value = extensions.map((ext) => ({
|
||||
id: ext.key_hash,
|
||||
name: ext.display_name || ext.name,
|
||||
version: ext.version,
|
||||
author: ext.namespace,
|
||||
icon: null,
|
||||
enabled: true,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Extensions:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const isExtensionInstalledAsync = async (
|
||||
extension: Partial<IHaexHubExtension>,
|
||||
) => {
|
||||
/* const loadExtensionsAsync = async () => {
|
||||
const { currentVault } = storeToRefs(useVaultStore())
|
||||
|
||||
const extensions =
|
||||
(await currentVault.value?.drizzle.select().from(haexExtensions)) ?? []
|
||||
|
||||
//if (!extensions?.length) return false;
|
||||
|
||||
const installedExtensions = await filterAsync(
|
||||
extensions,
|
||||
isExtensionInstalledAsync,
|
||||
)
|
||||
console.log('loadExtensionsAsync installedExtensions', installedExtensions)
|
||||
|
||||
availableExtensions.value =
|
||||
extensions.map((extension) => ({
|
||||
id: extension.id,
|
||||
name: extension.name ?? '',
|
||||
icon: extension.icon ?? '',
|
||||
author: extension.author ?? '',
|
||||
version: extension.version ?? '',
|
||||
enabled: extension.enabled ? true : false,
|
||||
installed: installedExtensions.includes(extension),
|
||||
})) ?? []
|
||||
|
||||
console.log('loadExtensionsAsync', availableExtensions.value)
|
||||
return true
|
||||
} */
|
||||
|
||||
const installAsync = async (sourcePath: string | null) => {
|
||||
if (!sourcePath) throw new Error('Kein Pfad angegeben')
|
||||
|
||||
try {
|
||||
const extensionPath = await getExtensionPathAsync(
|
||||
extension.id,
|
||||
`${extension.version}`,
|
||||
)
|
||||
console.log(
|
||||
`extension ${extension.id} is installed ${await exists(extensionPath)}`,
|
||||
)
|
||||
return await exists(extensionPath)
|
||||
const extensionId = await invoke<string>('install_extension', {
|
||||
sourcePath,
|
||||
})
|
||||
return extensionId
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error('Fehler bei Extension-Installation:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/* const installAsync = async (extensionDirectory: string | null) => {
|
||||
try {
|
||||
if (!extensionDirectory)
|
||||
throw new Error('Kein Ordner für Erweiterung angegeben')
|
||||
const manifestPath = await join(extensionDirectory, manifestFileName)
|
||||
const manifest = (await JSON.parse(
|
||||
await readTextFile(manifestPath),
|
||||
)) as IHaexHubExtensionManifest
|
||||
|
||||
const destination = await getExtensionPathAsync(
|
||||
manifest.id,
|
||||
manifest.version,
|
||||
)
|
||||
|
||||
await checkSourceExtensionDirectoryAsync(extensionDirectory)
|
||||
|
||||
await invoke('copy_directory', {
|
||||
source: extensionDirectory,
|
||||
destination,
|
||||
})
|
||||
|
||||
const logoFilePath = await join(destination, logoFileName)
|
||||
const logo = await readTextFile(logoFilePath)
|
||||
|
||||
const { currentVault } = storeToRefs(useVaultStore())
|
||||
const res = await currentVault.value?.drizzle
|
||||
.insert(haexExtensions)
|
||||
.values({
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
author: manifest.author,
|
||||
enabled: true,
|
||||
url: manifest.url,
|
||||
version: manifest.version,
|
||||
icon: logo,
|
||||
})
|
||||
|
||||
console.log('insert extensions', res)
|
||||
addNotificationAsync({
|
||||
type: 'success',
|
||||
text: `${manifest.name} wurde installiert`,
|
||||
})
|
||||
} catch (error) {
|
||||
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
||||
throw error
|
||||
}
|
||||
} */
|
||||
|
||||
const removeExtensionAsync = async (extensionId: string, version: string) => {
|
||||
try {
|
||||
await invoke('remove_extension', {
|
||||
extensionId,
|
||||
extensionVersion: version,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Entfernen der Extension:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/* const removeExtensionAsync = async (id: string, version: string) => {
|
||||
try {
|
||||
console.log('remove extension', id, version)
|
||||
await removeExtensionFromVaultAsync(id, version)
|
||||
await removeExtensionFilesAsync(id, version)
|
||||
} catch (error) {
|
||||
throw new Error(JSON.stringify(error))
|
||||
}
|
||||
} */
|
||||
|
||||
const isExtensionInstalledAsync = async ({
|
||||
id,
|
||||
version,
|
||||
}: {
|
||||
id: string
|
||||
version: string
|
||||
}) => {
|
||||
try {
|
||||
return await invoke<boolean>('is_extension_installed', {
|
||||
extensionId: id,
|
||||
extensionVersion: version,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Prüfen der Extension:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -156,7 +302,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
return true
|
||||
}
|
||||
|
||||
const readManifestFileAsync = async (
|
||||
/* const readManifestFileAsync = async (
|
||||
extensionId: string,
|
||||
version: string,
|
||||
) => {
|
||||
@ -173,173 +319,17 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
await readTextFile(manifestPath),
|
||||
)) as IHaexHubExtensionManifest
|
||||
|
||||
/*
|
||||
TODO implement check, that manifest has valid data
|
||||
*/
|
||||
return manifest
|
||||
} catch (error) {
|
||||
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
||||
console.error('ERROR readManifestFileAsync', error)
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
const installAsync = async (extensionDirectory: string | null) => {
|
||||
try {
|
||||
if (!extensionDirectory)
|
||||
throw new Error('Kein Ordner für Erweiterung angegeben')
|
||||
const manifestPath = await join(extensionDirectory, manifestFileName)
|
||||
const manifest = (await JSON.parse(
|
||||
await readTextFile(manifestPath),
|
||||
)) as IHaexHubExtensionManifest
|
||||
|
||||
const destination = await getExtensionPathAsync(
|
||||
manifest.id,
|
||||
manifest.version,
|
||||
)
|
||||
|
||||
await checkSourceExtensionDirectoryAsync(extensionDirectory)
|
||||
|
||||
await invoke('copy_directory', {
|
||||
source: extensionDirectory,
|
||||
destination,
|
||||
})
|
||||
|
||||
const logoFilePath = await join(destination, logoFileName)
|
||||
const logo = await readTextFile(logoFilePath)
|
||||
|
||||
const { currentVault } = storeToRefs(useVaultStore())
|
||||
const res = await currentVault.value?.drizzle
|
||||
.insert(haexExtensions)
|
||||
.values({
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
author: manifest.author,
|
||||
enabled: true,
|
||||
url: manifest.url,
|
||||
version: manifest.version,
|
||||
icon: logo,
|
||||
})
|
||||
|
||||
console.log('insert extensions', res)
|
||||
addNotificationAsync({
|
||||
type: 'success',
|
||||
text: `${manifest.name} wurde installiert`,
|
||||
})
|
||||
} catch (error) {
|
||||
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
||||
throw error
|
||||
/*
|
||||
const resourcePath = await resourceDir();
|
||||
//const manifestPath = await join(extensionDirectory, 'manifest.json');
|
||||
const manifestPath = await join(
|
||||
resourcePath,
|
||||
'extension',
|
||||
'demo-addon',
|
||||
'manifest.json'
|
||||
);
|
||||
const regex = /((href|src)=["'])([^"']+)(["'])/g;
|
||||
let htmlContent = await readTextFile(
|
||||
await join(resourcePath, 'extension', 'demo-addon', 'index.html')
|
||||
);
|
||||
|
||||
const replacements = [];
|
||||
let match;
|
||||
while ((match = regex.exec(htmlContent)) !== null) {
|
||||
const [fullMatch, prefix, attr, resource, suffix] = match;
|
||||
if (!resource.startsWith('http')) {
|
||||
replacements.push({ match: fullMatch, resource, prefix, suffix });
|
||||
}
|
||||
}
|
||||
|
||||
for (const { match, resource, prefix, suffix } of replacements) {
|
||||
const fileContent = await readTextFile(
|
||||
await join(resourcePath, 'extension', 'demo-addon', resource)
|
||||
);
|
||||
const blob = new Blob([fileContent], { type: getMimeType(resource) });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
console.log('blob', resource, blobUrl);
|
||||
htmlContent = htmlContent.replace(
|
||||
match,
|
||||
`${prefix}${blobUrl}${suffix}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('htmlContent', htmlContent);
|
||||
|
||||
const blob = new Blob([htmlContent], { type: 'text/html' });
|
||||
const iframeSrc = URL.createObjectURL(blob);
|
||||
|
||||
const manifestContent = await readTextFile(manifestPath);
|
||||
console.log('iframeSrc', iframeSrc);
|
||||
const manifest: PluginManifest = JSON.parse(manifestContent);
|
||||
//const entryPath = await join(extensionDirectory, manifest.entry);
|
||||
const entryPath = await join(
|
||||
resourcePath,
|
||||
'extension',
|
||||
'demo-addon',
|
||||
manifest.entry
|
||||
);
|
||||
console.log('extensionDirectory', extensionDirectory, entryPath);
|
||||
const path = convertFileSrc(extensionDirectory, manifest.entry);
|
||||
console.log('final path', path);
|
||||
manifest.entry = iframeSrc;
|
||||
/* await join(
|
||||
path, //`file:/${extensionDirectory}`,
|
||||
manifest.entry
|
||||
); */
|
||||
// Modul-Datei laden
|
||||
//const modulePathFull = await join(basePath, manifest.main);
|
||||
/* const manifest: PluginManifest = await invoke('load_plugin', {
|
||||
manifestPath,
|
||||
}); */
|
||||
/* const iframe = document.createElement('iframe');
|
||||
iframe.src = manifest.entry;
|
||||
iframe.setAttribute('sandbox', 'allow-scripts');
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.height = '100%';
|
||||
iframe.style.border = 'none'; */
|
||||
/* const addonApi = {
|
||||
db_execute: async (sql: string, params: string[] = []) => {
|
||||
return invoke('db_execute', {
|
||||
addonId: manifest.name,
|
||||
sql,
|
||||
params,
|
||||
});
|
||||
},
|
||||
db_select: async (sql: string, params: string[] = []) => {
|
||||
return invoke('db_select', {
|
||||
addonId: manifest.name,
|
||||
sql,
|
||||
params,
|
||||
});
|
||||
},
|
||||
}; */
|
||||
/* iframe.onload = () => {
|
||||
iframe.contentWindow?.postMessage(
|
||||
{ type: 'init', payload: addonApi },
|
||||
'*'
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source === iframe.contentWindow) {
|
||||
const { type } = event.data;
|
||||
if (type === 'ready') {
|
||||
console.log(`Plugin ${manifest.name} ist bereit`);
|
||||
}
|
||||
}
|
||||
}); */
|
||||
/* plugins.value.push({ name: manifest.name, entry: manifest.entry });
|
||||
|
||||
console.log(`Plugin ${manifest.name} geladen.`); */
|
||||
}
|
||||
}
|
||||
|
||||
const extensionEntry = computedAsync(
|
||||
/* const extensionEntry = computedAsync(
|
||||
async () => {
|
||||
try {
|
||||
/* console.log("extensionEntry start", currentExtension.value);
|
||||
const regex = /((href|src)=["'])([^"']+)(["'])/g; */
|
||||
|
||||
|
||||
if (!currentExtension.value?.id || !currentExtension.value.version) {
|
||||
console.log('extension id or entry missing', currentExtension.value)
|
||||
@ -375,60 +365,19 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
},
|
||||
null,
|
||||
{ lazy: true },
|
||||
)
|
||||
|
||||
const loadExtensionsAsync = async () => {
|
||||
const { currentVault } = storeToRefs(useVaultStore())
|
||||
|
||||
const extensions =
|
||||
(await currentVault.value?.drizzle.select().from(haexExtensions)) ?? []
|
||||
|
||||
//if (!extensions?.length) return false;
|
||||
|
||||
const installedExtensions = await filterAsync(
|
||||
extensions,
|
||||
isExtensionInstalledAsync,
|
||||
)
|
||||
console.log('loadExtensionsAsync installedExtensions', installedExtensions)
|
||||
|
||||
availableExtensions.value =
|
||||
extensions.map((extension) => ({
|
||||
id: extension.id,
|
||||
name: extension.name ?? '',
|
||||
icon: extension.icon ?? '',
|
||||
author: extension.author ?? '',
|
||||
version: extension.version ?? '',
|
||||
enabled: extension.enabled ? true : false,
|
||||
installed: installedExtensions.includes(extension),
|
||||
})) ?? []
|
||||
|
||||
console.log('loadExtensionsAsync', availableExtensions.value)
|
||||
return true
|
||||
}
|
||||
|
||||
const removeExtensionAsync = async (id: string, version: string) => {
|
||||
try {
|
||||
console.log('remove extension', id, version)
|
||||
await removeExtensionFromVaultAsync(id, version)
|
||||
await removeExtensionFilesAsync(id, version)
|
||||
} catch (error) {
|
||||
throw new Error(JSON.stringify(error))
|
||||
}
|
||||
}
|
||||
) */
|
||||
|
||||
return {
|
||||
availableExtensions,
|
||||
checkManifest,
|
||||
currentExtension,
|
||||
currentExtensionId,
|
||||
extensionEntry,
|
||||
extensionLinks,
|
||||
installAsync,
|
||||
isActive,
|
||||
isExtensionInstalledAsync,
|
||||
loadExtensionsAsync,
|
||||
readManifestFileAsync,
|
||||
removeExtensionAsync,
|
||||
getExtensionPathAsync,
|
||||
}
|
||||
})
|
||||
|
||||
@ -438,7 +387,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
return 'text/plain'
|
||||
} */
|
||||
|
||||
const removeExtensionFromVaultAsync = async (
|
||||
/* const removeExtensionFromVaultAsync = async (
|
||||
id: string | null,
|
||||
version: string | null,
|
||||
) => {
|
||||
@ -457,9 +406,9 @@ const removeExtensionFromVaultAsync = async (
|
||||
.delete(haexExtensions)
|
||||
.where(and(eq(haexExtensions.id, id), eq(haexExtensions.version, version)))
|
||||
return removedExtensions
|
||||
}
|
||||
} */
|
||||
|
||||
const removeExtensionFilesAsync = async (
|
||||
/* const removeExtensionFilesAsync = async (
|
||||
id: string | null,
|
||||
version: string | null,
|
||||
) => {
|
||||
@ -483,4 +432,13 @@ const removeExtensionFilesAsync = async (
|
||||
console.error('ERROR removeExtensionFilesAsync', error)
|
||||
throw new Error(JSON.stringify(error))
|
||||
}
|
||||
} */
|
||||
|
||||
function encodeExtensionInfo(id: string, version: string): string {
|
||||
const info = { id, version }
|
||||
const jsonString = JSON.stringify(info)
|
||||
const bytes = new TextEncoder().encode(jsonString)
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
}
|
||||
|
||||
143
src/stores/extensions/tabs.ts
Normal file
143
src/stores/extensions/tabs.ts
Normal file
@ -0,0 +1,143 @@
|
||||
// stores/extensions/tabs.ts
|
||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||
|
||||
interface ExtensionTab {
|
||||
extension: IHaexHubExtension
|
||||
iframe: HTMLIFrameElement | null
|
||||
isVisible: boolean
|
||||
lastAccessed: number
|
||||
}
|
||||
|
||||
export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
|
||||
// State
|
||||
const openTabs = ref(new Map<string, ExtensionTab>())
|
||||
const activeTabId = ref<string | null>(null)
|
||||
|
||||
// Getters
|
||||
const activeTab = computed(() => {
|
||||
if (!activeTabId.value) return null
|
||||
return openTabs.value.get(activeTabId.value) || null
|
||||
})
|
||||
|
||||
const tabCount = computed(() => openTabs.value.size)
|
||||
|
||||
const sortedTabs = computed(() => {
|
||||
return Array.from(openTabs.value.values()).sort(
|
||||
(a, b) => b.lastAccessed - a.lastAccessed,
|
||||
)
|
||||
})
|
||||
|
||||
// Actions
|
||||
const openTab = (extensionId: string) => {
|
||||
// Hole Extension-Info aus dem anderen Store
|
||||
const extensionsStore = useExtensionsStore()
|
||||
const extension = extensionsStore.availableExtensions.find(
|
||||
(ext) => ext.id === extensionId,
|
||||
)
|
||||
|
||||
if (!extension) {
|
||||
console.error(`Extension ${extensionId} nicht gefunden`)
|
||||
return
|
||||
}
|
||||
|
||||
// Bereits geöffnet? Nur aktivieren
|
||||
if (openTabs.value.has(extensionId)) {
|
||||
setActiveTab(extensionId)
|
||||
return
|
||||
}
|
||||
|
||||
// Limit: Max 10 Tabs
|
||||
if (openTabs.value.size >= 10) {
|
||||
const oldestInactive = sortedTabs.value
|
||||
.filter((tab) => tab.extension.id !== activeTabId.value)
|
||||
.pop()
|
||||
|
||||
if (oldestInactive) {
|
||||
closeTab(oldestInactive.extension.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Neuen Tab erstellen
|
||||
openTabs.value.set(extensionId, {
|
||||
extension,
|
||||
iframe: null,
|
||||
isVisible: false,
|
||||
lastAccessed: Date.now(),
|
||||
})
|
||||
|
||||
setActiveTab(extensionId)
|
||||
}
|
||||
|
||||
const setActiveTab = (extensionId: string) => {
|
||||
// Verstecke aktuellen Tab
|
||||
if (activeTabId.value && openTabs.value.has(activeTabId.value)) {
|
||||
const currentTab = openTabs.value.get(activeTabId.value)!
|
||||
currentTab.isVisible = false
|
||||
}
|
||||
|
||||
// Zeige neuen Tab
|
||||
const newTab = openTabs.value.get(extensionId)
|
||||
if (newTab) {
|
||||
newTab.isVisible = true
|
||||
newTab.lastAccessed = Date.now()
|
||||
activeTabId.value = extensionId
|
||||
}
|
||||
}
|
||||
|
||||
const closeTab = (extensionId: string) => {
|
||||
const tab = openTabs.value.get(extensionId)
|
||||
if (!tab) return
|
||||
|
||||
// IFrame entfernen
|
||||
tab.iframe?.remove()
|
||||
openTabs.value.delete(extensionId)
|
||||
|
||||
// Nächsten Tab aktivieren
|
||||
if (activeTabId.value === extensionId) {
|
||||
const remaining = sortedTabs.value
|
||||
const nextTab = remaining[0]
|
||||
|
||||
if (nextTab) {
|
||||
setActiveTab(nextTab.extension.id)
|
||||
} else {
|
||||
activeTabId.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const registerIFrame = (extensionId: string, iframe: HTMLIFrameElement) => {
|
||||
const tab = openTabs.value.get(extensionId)
|
||||
if (tab) {
|
||||
tab.iframe = iframe
|
||||
}
|
||||
}
|
||||
|
||||
const broadcastToAllTabs = (message: unknown) => {
|
||||
openTabs.value.forEach(({ iframe }) => {
|
||||
iframe?.contentWindow?.postMessage(message, '*')
|
||||
})
|
||||
}
|
||||
|
||||
const closeAllTabs = () => {
|
||||
openTabs.value.forEach((tab) => tab.iframe?.remove())
|
||||
openTabs.value.clear()
|
||||
activeTabId.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
openTabs,
|
||||
activeTabId,
|
||||
// Getters
|
||||
activeTab,
|
||||
tabCount,
|
||||
sortedTabs,
|
||||
// Actions
|
||||
openTab,
|
||||
setActiveTab,
|
||||
closeTab,
|
||||
registerIFrame,
|
||||
broadcastToAllTabs,
|
||||
closeAllTabs,
|
||||
}
|
||||
})
|
||||
11
src/types/haexhub.d.ts
vendored
11
src/types/haexhub.d.ts
vendored
@ -25,11 +25,10 @@ export interface IHaexHubExtensionLink extends IHaexHubExtension {
|
||||
}
|
||||
|
||||
export interface IHaexHubExtension {
|
||||
author?: string | null
|
||||
enabled?: boolean | null
|
||||
icon?: string | null
|
||||
id: string
|
||||
manifest?: IHaexHubExtensionManifest
|
||||
name: string | null
|
||||
version?: string | null
|
||||
name: string
|
||||
version: string
|
||||
author: string | null
|
||||
icon: string | null
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user