mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +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
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
@ -99,7 +99,7 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
//plugins: [tailwindcss()],
|
||||||
// Better support for Tauri CLI output
|
// Better support for Tauri CLI output
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
// Enable environment variables
|
// 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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
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]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@ -62,6 +73,15 @@ version = "1.0.97"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
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]]
|
[[package]]
|
||||||
name = "ashpd"
|
name = "ashpd"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -274,6 +294,12 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
@ -386,6 +412,15 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "cairo-rs"
|
name = "cairo-rs"
|
||||||
version = "0.18.5"
|
version = "0.18.5"
|
||||||
@ -440,7 +475,7 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -459,6 +494,8 @@ version = "1.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
|
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -514,6 +551,16 @@ dependencies = [
|
|||||||
"windows-link",
|
"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]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@ -533,6 +580,18 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"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]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -627,6 +686,21 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
@ -698,6 +772,33 @@ dependencies = [
|
|||||||
"syn 2.0.100",
|
"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]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.20.10"
|
version = "0.20.10"
|
||||||
@ -739,6 +840,22 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
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]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -749,6 +866,17 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "0.99.19"
|
version = "0.99.19"
|
||||||
@ -770,6 +898,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -890,6 +1019,30 @@ version = "1.0.19"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
|
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]]
|
[[package]]
|
||||||
name = "embed-resource"
|
name = "embed-resource"
|
||||||
version = "3.0.2"
|
version = "3.0.2"
|
||||||
@ -1020,6 +1173,12 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fiat-crypto"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "field-offset"
|
name = "field-offset"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
@ -1032,11 +1191,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.0"
|
version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
|
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
|
"libz-rs-sys",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1081,9 +1241,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.1"
|
version = "1.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
@ -1533,6 +1693,7 @@ name = "haex-hub"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
"ed25519-dalek",
|
||||||
"fs_extra",
|
"fs_extra",
|
||||||
"hex",
|
"hex",
|
||||||
"mime",
|
"mime",
|
||||||
@ -1540,10 +1701,10 @@ dependencies = [
|
|||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"sqlparser",
|
"sqlparser",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-android-fs",
|
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
"tauri-plugin-http",
|
"tauri-plugin-http",
|
||||||
@ -1552,11 +1713,12 @@ dependencies = [
|
|||||||
"tauri-plugin-os",
|
"tauri-plugin-os",
|
||||||
"tauri-plugin-persisted-scope",
|
"tauri-plugin-persisted-scope",
|
||||||
"tauri-plugin-store",
|
"tauri-plugin-store",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
"uhlc",
|
"uhlc",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1607,6 +1769,15 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html5ever"
|
name = "html5ever"
|
||||||
version = "0.29.1"
|
version = "0.29.1"
|
||||||
@ -1887,9 +2058,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.0.3"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
|
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"idna_adapter",
|
"idna_adapter",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@ -1937,6 +2108,15 @@ dependencies = [
|
|||||||
"cfb",
|
"cfb",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "io-uring"
|
name = "io-uring"
|
||||||
version = "0.7.8"
|
version = "0.7.8"
|
||||||
@ -2024,6 +2204,16 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
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]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.77"
|
version = "0.3.77"
|
||||||
@ -2109,6 +2299,12 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libbz2-rs-sys"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.175"
|
version = "0.2.175"
|
||||||
@ -2147,6 +2343,15 @@ dependencies = [
|
|||||||
"vcpkg",
|
"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]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.15"
|
version = "0.4.15"
|
||||||
@ -2187,6 +2392,16 @@ version = "0.4.26"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
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]]
|
[[package]]
|
||||||
name = "mac"
|
name = "mac"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@ -2305,7 +2520,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2775,10 +2990,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "pbkdf2"
|
||||||
version = "2.3.1"
|
version = "0.12.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "phf"
|
name = "phf"
|
||||||
@ -2937,6 +3162,16 @@ dependencies = [
|
|||||||
"futures-io",
|
"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]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@ -2990,6 +3225,12 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppmd-rust"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@ -3129,7 +3370,7 @@ dependencies = [
|
|||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"socket2 0.5.8",
|
"socket2 0.5.8",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"web-time",
|
"web-time",
|
||||||
@ -3149,7 +3390,7 @@ dependencies = [
|
|||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"slab",
|
"slab",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"tracing",
|
"tracing",
|
||||||
"web-time",
|
"web-time",
|
||||||
@ -3338,7 +3579,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.15",
|
"getrandom 0.2.15",
|
||||||
"libredox",
|
"libredox",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3646,9 +3887,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.226"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
@ -3667,18 +3908,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_core"
|
name = "serde_core"
|
||||||
version = "1.0.226"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.226"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3813,10 +4054,21 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha1"
|
||||||
version = "0.10.8"
|
version = "0.10.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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 = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
@ -3838,6 +4090,15 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@ -3946,10 +4207,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparser"
|
name = "spki"
|
||||||
version = "0.58.0"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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 = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"recursive",
|
"recursive",
|
||||||
@ -4224,7 +4495,7 @@ dependencies = [
|
|||||||
"tauri-runtime",
|
"tauri-runtime",
|
||||||
"tauri-runtime-wry",
|
"tauri-runtime-wry",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"url",
|
"url",
|
||||||
@ -4277,7 +4548,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"syn 2.0.100",
|
"syn 2.0.100",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"time",
|
"time",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
@ -4315,21 +4586,6 @@ dependencies = [
|
|||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-plugin-dialog"
|
name = "tauri-plugin-dialog"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
@ -4344,7 +4600,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4365,7 +4621,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"toml 0.9.5",
|
"toml 0.9.5",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
@ -4388,7 +4644,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"urlpattern",
|
"urlpattern",
|
||||||
@ -4408,7 +4664,7 @@ dependencies = [
|
|||||||
"serde_repr",
|
"serde_repr",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"time",
|
"time",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
@ -4429,7 +4685,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"url",
|
"url",
|
||||||
"windows",
|
"windows",
|
||||||
"zbus",
|
"zbus",
|
||||||
@ -4450,7 +4706,7 @@ dependencies = [
|
|||||||
"sys-locale",
|
"sys-locale",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4466,7 +4722,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4480,7 +4736,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
@ -4503,7 +4759,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
@ -4567,7 +4823,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"swift-rs",
|
"swift-rs",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"toml 0.9.5",
|
"toml 0.9.5",
|
||||||
"url",
|
"url",
|
||||||
"urlpattern",
|
"urlpattern",
|
||||||
@ -4592,7 +4848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
|
checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quick-xml 0.37.5",
|
"quick-xml 0.37.5",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"windows",
|
"windows",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
]
|
]
|
||||||
@ -4641,11 +4897,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.16"
|
version = "2.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
|
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 2.0.16",
|
"thiserror-impl 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4661,9 +4917,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.16"
|
version = "2.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
|
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -4951,7 +5207,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4967,7 +5223,7 @@ version = "11.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be"
|
checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"ts-rs-macros",
|
"ts-rs-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -5087,9 +5343,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.4"
|
version = "2.5.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
|
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
@ -5405,7 +5661,7 @@ version = "0.38.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
|
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"windows",
|
"windows",
|
||||||
"windows-core 0.61.0",
|
"windows-core 0.61.0",
|
||||||
]
|
]
|
||||||
@ -5965,7 +6221,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"soup3",
|
"soup3",
|
||||||
"tao-macros",
|
"tao-macros",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.17",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webkit2gtk-sys",
|
"webkit2gtk-sys",
|
||||||
@ -6128,6 +6384,20 @@ name = "zeroize"
|
|||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
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]]
|
[[package]]
|
||||||
name = "zerovec"
|
name = "zerovec"
|
||||||
@ -6151,6 +6421,79 @@ dependencies = [
|
|||||||
"syn 2.0.100",
|
"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]]
|
[[package]]
|
||||||
name = "zvariant"
|
name = "zvariant"
|
||||||
version = "5.7.0"
|
version = "5.7.0"
|
||||||
|
|||||||
@ -18,37 +18,38 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
|
|
||||||
tauri-build = { version = "2.2", features = [] }
|
tauri-build = { version = "2.2", features = [] }
|
||||||
serde = { version = "1.0.226", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rusqlite = { version = "0.37.0", features = [
|
rusqlite = { version = "0.37.0", features = [
|
||||||
"load_extension",
|
"load_extension",
|
||||||
"bundled-sqlcipher-vendored-openssl",
|
"bundled-sqlcipher-vendored-openssl",
|
||||||
"functions",
|
"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"] }
|
#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"
|
base64 = "0.22"
|
||||||
mime_guess = "2.0"
|
ed25519-dalek = "2.1"
|
||||||
mime = "0.3"
|
|
||||||
fs_extra = "1.3.0"
|
fs_extra = "1.3.0"
|
||||||
sqlparser = { version = "0.58.0", features = ["visitor"] }
|
hex = "0.4"
|
||||||
uhlc = "0.8"
|
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 = { version = "2.8.5", features = ["protocol-asset", "devtools"] }
|
||||||
tauri-plugin-dialog = "2.4.0"
|
tauri-plugin-dialog = "2.4.0"
|
||||||
tauri-plugin-fs = "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-http = "2.5.2"
|
||||||
tauri-plugin-notification = "2.3.1"
|
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-persisted-scope = "2.3.2"
|
||||||
tauri-plugin-android-fs = "12.0.1"
|
tauri-plugin-store = "2.4.0"
|
||||||
uuid = { version = "1.18.1", features = ["v4"] }
|
thiserror = "2.0.17"
|
||||||
ts-rs = "11.0.1"
|
ts-rs = "11.0.1"
|
||||||
thiserror = "2.0.16"
|
uhlc = "0.8"
|
||||||
|
uuid = { version = "1.18.1", features = ["v4"] }
|
||||||
#tauri-plugin-sql = { version = "2", features = ["sqlite"] }
|
zip = "5.1.1"
|
||||||
|
url = "2.5.7"
|
||||||
|
|||||||
@ -25,7 +25,6 @@
|
|||||||
"fs:allow-download-read-recursive",
|
"fs:allow-download-read-recursive",
|
||||||
"fs:allow-download-write-recursive",
|
"fs:allow-download-write-recursive",
|
||||||
"fs:default",
|
"fs:default",
|
||||||
"android-fs:default",
|
|
||||||
{
|
{
|
||||||
"identifier": "fs:scope",
|
"identifier": "fs:scope",
|
||||||
"allow": [{ "path": "**" }]
|
"allow": [{ "path": "**" }]
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { drizzle } from 'drizzle-orm/sqlite-proxy' // Adapter für Query Building ohne direkte Verbindung
|
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.
|
// sqlite-proxy benötigt eine (dummy) Ausführungsfunktion als Argument.
|
||||||
// Diese wird in unserem Tauri-Workflow nie aufgerufen, da wir nur .toSQL() verwenden.
|
// 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,
|
"when": 1757968140525,
|
||||||
"tag": "0011_illegal_thor_girl",
|
"tag": "0011_illegal_thor_girl",
|
||||||
"breakpoints": true
|
"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,
|
primaryKey,
|
||||||
sqliteTable,
|
sqliteTable,
|
||||||
text,
|
text,
|
||||||
unique,
|
|
||||||
type AnySQLiteColumn,
|
type AnySQLiteColumn,
|
||||||
} from 'drizzle-orm/sqlite-core'
|
} from 'drizzle-orm/sqlite-core'
|
||||||
import tableNames from '../tableNames.json'
|
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, {
|
export const haexNotifications = sqliteTable(tableNames.haex.notifications, {
|
||||||
id: text().primaryKey(),
|
id: text().primaryKey(),
|
||||||
alt: text(),
|
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": {
|
"Identifier": {
|
||||||
"description": "Permission identifier",
|
"description": "Permission identifier",
|
||||||
"oneOf": [
|
"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`",
|
"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",
|
"type": "string",
|
||||||
|
|||||||
@ -2270,12 +2270,6 @@
|
|||||||
"Identifier": {
|
"Identifier": {
|
||||||
"description": "Permission identifier",
|
"description": "Permission identifier",
|
||||||
"oneOf": [
|
"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`",
|
"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",
|
"type": "string",
|
||||||
|
|||||||
@ -10,8 +10,8 @@ use std::{
|
|||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tauri::{AppHandle, Wry};
|
use tauri::AppHandle;
|
||||||
use tauri_plugin_store::{Store, StoreExt};
|
use tauri_plugin_store::StoreExt;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uhlc::{HLCBuilder, Timestamp, HLC, ID};
|
use uhlc::{HLCBuilder, Timestamp, HLC, ID};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|||||||
@ -755,6 +755,7 @@ impl CrdtTransformer {
|
|||||||
selection: del_stmt.selection.clone(),
|
selection: del_stmt.selection.clone(),
|
||||||
returning: None,
|
returning: None,
|
||||||
or: None,
|
or: None,
|
||||||
|
limit: None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
// src-tauri/src/crdt/trigger.rs
|
||||||
use crate::table_names::TABLE_CRDT_LOGS;
|
use crate::table_names::TABLE_CRDT_LOGS;
|
||||||
use rusqlite::{Connection, Result as RusqliteResult, Row, Transaction};
|
use rusqlite::{Connection, Result as RusqliteResult, Row, Transaction};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@ -11,7 +12,7 @@ const UPDATE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_update";
|
|||||||
|
|
||||||
//const SYNC_ACTIVE_KEY: &str = "sync_active";
|
//const SYNC_ACTIVE_KEY: &str = "sync_active";
|
||||||
pub const TOMBSTONE_COLUMN: &str = "haex_tombstone";
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum CrdtSetupError {
|
pub enum CrdtSetupError {
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
// src-tauri/src/database/core.rs
|
// src-tauri/src/database/core.rs
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
use crate::database::DbConnection;
|
use crate::database::DbConnection;
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
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::ast::{Query, Select, SetExpr, Statement, TableFactor, TableObject};
|
||||||
use sqlparser::dialect::SQLiteDialect;
|
use sqlparser::dialect::SQLiteDialect;
|
||||||
use sqlparser::parser::Parser;
|
use sqlparser::parser::Parser;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Öffnet und initialisiert eine Datenbank mit Verschlüsselung
|
/// Öffnet und initialisiert eine Datenbank mit Verschlüsselung
|
||||||
pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connection, DatabaseError> {
|
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::Table(_)
|
||||||
| SetExpr::Insert(_)
|
| SetExpr::Insert(_)
|
||||||
| SetExpr::Update(_)
|
| SetExpr::Update(_)
|
||||||
|
| SetExpr::Merge(_)
|
||||||
| SetExpr::Delete(_) => {}
|
| 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
|
// src-tauri/src/extension/database/mod.rs
|
||||||
|
|
||||||
pub mod permissions;
|
pub mod executor;
|
||||||
use crate::crdt::hlc::HlcService;
|
use crate::crdt::hlc::HlcService;
|
||||||
use crate::crdt::transformer::CrdtTransformer;
|
use crate::crdt::transformer::CrdtTransformer;
|
||||||
use crate::crdt::trigger;
|
use crate::crdt::trigger;
|
||||||
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
use crate::extension::error::ExtensionError;
|
use crate::extension::error::ExtensionError;
|
||||||
|
use crate::extension::permissions::validator::SqlPermissionValidator;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use permissions::{check_read_permission, check_write_permission};
|
|
||||||
use rusqlite::params_from_iter;
|
use rusqlite::params_from_iter;
|
||||||
use rusqlite::types::Value as SqlValue;
|
use rusqlite::types::Value as SqlValue;
|
||||||
use rusqlite::Transaction;
|
use rusqlite::Transaction;
|
||||||
@ -116,7 +117,7 @@ pub async fn extension_sql_execute(
|
|||||||
hlc_service: State<'_, HlcService>,
|
hlc_service: State<'_, HlcService>,
|
||||||
) -> Result<Vec<String>, ExtensionError> {
|
) -> Result<Vec<String>, ExtensionError> {
|
||||||
// Permission check
|
// Permission check
|
||||||
check_write_permission(&state.db, &extension_id, sql).await?;
|
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
|
||||||
|
|
||||||
// Parameter validation
|
// Parameter validation
|
||||||
validate_params(sql, ¶ms)?;
|
validate_params(sql, ¶ms)?;
|
||||||
@ -186,7 +187,7 @@ pub async fn extension_sql_select(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<Vec<JsonValue>, ExtensionError> {
|
) -> Result<Vec<JsonValue>, ExtensionError> {
|
||||||
// Permission check
|
// Permission check
|
||||||
check_read_permission(&state.db, &extension_id, sql).await?;
|
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
|
||||||
|
|
||||||
// Parameter validation
|
// Parameter validation
|
||||||
validate_params(sql, ¶ms)?;
|
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 thiserror::Error;
|
||||||
|
|
||||||
use crate::database::error::DatabaseError;
|
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)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ExtensionError {
|
pub enum ExtensionError {
|
||||||
#[error("Security violation: {reason}")]
|
#[error("Security violation: {reason}")]
|
||||||
@ -29,15 +56,10 @@ pub enum ExtensionError {
|
|||||||
Filesystem {
|
Filesystem {
|
||||||
#[from]
|
#[from]
|
||||||
source: std::io::Error,
|
source: std::io::Error,
|
||||||
// oder: source: FilesystemError,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("HTTP request failed: {reason}")]
|
#[error("HTTP request failed: {reason}")]
|
||||||
Http {
|
Http { reason: String },
|
||||||
reason: String,
|
|
||||||
#[source]
|
|
||||||
source: Option<Box<dyn std::error::Error + Send + Sync>>,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error("Shell command failed: {reason}")]
|
#[error("Shell command failed: {reason}")]
|
||||||
Shell {
|
Shell {
|
||||||
@ -45,29 +67,51 @@ pub enum ExtensionError {
|
|||||||
exit_code: Option<i32>,
|
exit_code: Option<i32>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/* #[error("IO error: {source}")]
|
|
||||||
Io {
|
|
||||||
#[from]
|
|
||||||
source: std::io::Error,
|
|
||||||
}, */
|
|
||||||
#[error("Manifest error: {reason}")]
|
#[error("Manifest error: {reason}")]
|
||||||
ManifestError { reason: String },
|
ManifestError { reason: String },
|
||||||
|
|
||||||
#[error("Validation error: {reason}")]
|
#[error("Validation error: {reason}")]
|
||||||
ValidationError { reason: String },
|
ValidationError { reason: String },
|
||||||
|
|
||||||
#[error("Dev server error: {reason}")]
|
#[error("Invalid Public Key: {reason}")]
|
||||||
DevServerError { reason: String },
|
InvalidPublicKey { reason: String },
|
||||||
|
|
||||||
#[error("Serialization error: {reason}")]
|
#[error("Invalid Signature: {reason}")]
|
||||||
SerializationError { reason: String },
|
InvalidSignature { reason: String },
|
||||||
|
|
||||||
#[error("Configuration error: {reason}")]
|
#[error("Error during hash calculation: {reason}")]
|
||||||
ConfigError { reason: String },
|
CalculateHashError { reason: String },
|
||||||
|
|
||||||
|
#[error("Signature verification failed: {reason}")]
|
||||||
|
SignatureVerificationFailed { reason: String },
|
||||||
|
|
||||||
|
#[error("Extension installation failed: {reason}")]
|
||||||
|
InstallationFailed { reason: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtensionError {
|
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 {
|
pub fn permission_denied(extension_id: &str, operation: &str, resource: &str) -> Self {
|
||||||
Self::PermissionDenied {
|
Self::PermissionDenied {
|
||||||
extension_id: extension_id.to_string(),
|
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 {
|
pub fn is_permission_error(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
@ -111,11 +127,9 @@ impl ExtensionError {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract extension ID if available
|
|
||||||
pub fn extension_id(&self) -> Option<&str> {
|
pub fn extension_id(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
ExtensionError::PermissionDenied { extension_id, .. } => Some(extension_id),
|
ExtensionError::PermissionDenied { extension_id, .. } => Some(extension_id),
|
||||||
ExtensionError::Database { source } => source.extension_id(),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,29 +142,12 @@ impl serde::Serialize for ExtensionError {
|
|||||||
{
|
{
|
||||||
use serde::ser::SerializeStruct;
|
use serde::ser::SerializeStruct;
|
||||||
|
|
||||||
let mut state = serializer.serialize_struct("ExtensionError", 3)?;
|
let mut state = serializer.serialize_struct("ExtensionError", 4)?;
|
||||||
|
|
||||||
// Error type as discriminator
|
state.serialize_field("code", &self.code())?;
|
||||||
let error_type = match self {
|
state.serialize_field("type", &format!("{:?}", 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("message", &self.to_string())?;
|
state.serialize_field("message", &self.to_string())?;
|
||||||
|
|
||||||
// Add extension_id if available
|
|
||||||
if let Some(ext_id) = self.extension_id() {
|
if let Some(ext_id) = self.extension_id() {
|
||||||
state.serialize_field("extension_id", ext_id)?;
|
state.serialize_field("extension_id", ext_id)?;
|
||||||
} else {
|
} 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 {
|
impl From<serde_json::Error> for ExtensionError {
|
||||||
fn from(err: serde_json::Error) -> Self {
|
fn from(err: serde_json::Error) -> Self {
|
||||||
ExtensionError::SerializationError {
|
ExtensionError::ManifestError {
|
||||||
reason: err.to_string(),
|
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};
|
/// src-tauri/src/extension/mod.rs
|
||||||
use tauri::State;
|
use crate::{
|
||||||
|
extension::{
|
||||||
|
core::{EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
||||||
|
error::ExtensionError,
|
||||||
|
},
|
||||||
|
AppState,
|
||||||
|
};
|
||||||
|
use tauri::{AppHandle, State};
|
||||||
pub mod core;
|
pub mod core;
|
||||||
|
pub mod crypto;
|
||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod filesystem;
|
pub mod filesystem;
|
||||||
pub mod permission_manager;
|
pub mod permissions;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_extension_info(
|
pub fn get_extension_info(
|
||||||
extension_id: String,
|
extension_id: String,
|
||||||
extension_manager: State<ExtensionManager>,
|
state: State<AppState>,
|
||||||
) -> Result<ExtensionInfoResponse, String> {
|
) -> Result<ExtensionInfoResponse, String> {
|
||||||
let extension = extension_manager
|
let extension = state
|
||||||
|
.extension_manager
|
||||||
.get_extension(&extension_id)
|
.get_extension(&extension_id)
|
||||||
.ok_or_else(|| format!("Extension nicht gefunden: {}", 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 crdt;
|
||||||
mod database;
|
mod database;
|
||||||
mod extension;
|
mod extension;
|
||||||
|
use crate::{
|
||||||
//mod models;
|
crdt::hlc::HlcService,
|
||||||
|
database::DbConnection,
|
||||||
|
extension::core::{ExtensionManager, ExtensionState},
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
pub mod table_names {
|
pub mod table_names {
|
||||||
include!(concat!(env!("OUT_DIR"), "/tableNames.rs"));
|
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 struct AppState {
|
||||||
pub db: DbConnection,
|
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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@ -31,26 +25,27 @@ pub fn run() {
|
|||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.register_uri_scheme_protocol(protocol_name, move |context, request| {
|
.register_uri_scheme_protocol(protocol_name, move |context, request| {
|
||||||
match extension::core::extension_protocol_handler(&context, &request) {
|
// Hole den AppState aus dem Context
|
||||||
Ok(response) => response, // Wenn der Handler Ok ist, gib die Response direkt zurück
|
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) => {
|
Err(e) => {
|
||||||
// Wenn der Handler einen Fehler zurückgibt, logge ihn und erstelle eine Fehler-Response
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Fehler im Custom Protocol Handler für URI '{}': {}",
|
"Fehler im Custom Protocol Handler für URI '{}': {}",
|
||||||
request.uri(),
|
request.uri(),
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
// Erstelle eine HTTP 500 Fehler-Response
|
|
||||||
// Du kannst hier auch spezifischere Fehler-Responses bauen, falls gewünscht.
|
|
||||||
tauri::http::Response::builder()
|
tauri::http::Response::builder()
|
||||||
.status(500)
|
.status(500)
|
||||||
.header("Content-Type", "text/plain") // Optional, aber gut für Klarheit
|
.header("Content-Type", "text/plain")
|
||||||
.body(Vec::from(format!(
|
.body(Vec::from(format!(
|
||||||
"Interner Serverfehler im Protokollhandler: {}",
|
"Interner Serverfehler im Protokollhandler: {}",
|
||||||
e
|
e
|
||||||
)))
|
)))
|
||||||
.unwrap_or_else(|build_err| {
|
.unwrap_or_else(|build_err| {
|
||||||
// Fallback, falls selbst das Erstellen der Fehler-Response fehlschlägt
|
|
||||||
eprintln!("Konnte Fehler-Response nicht erstellen: {}", build_err);
|
eprintln!("Konnte Fehler-Response nicht erstellen: {}", build_err);
|
||||||
tauri::http::Response::builder()
|
tauri::http::Response::builder()
|
||||||
.status(500)
|
.status(500)
|
||||||
@ -60,11 +55,10 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
/* .manage(database::DbConnection(Arc::new(Mutex::new(None))))
|
|
||||||
.manage(crdt::hlc::HlcService::new()) */
|
|
||||||
.manage(AppState {
|
.manage(AppState {
|
||||||
db: DbConnection(Arc::new(Mutex::new(None))),
|
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())
|
.manage(ExtensionState::default())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
@ -75,7 +69,6 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_persisted_scope::init())
|
.plugin(tauri_plugin_persisted_scope::init())
|
||||||
.plugin(tauri_plugin_store::Builder::new().build())
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
//.plugin(tauri_plugin_android_fs::init())
|
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
database::create_encrypted_database,
|
database::create_encrypted_database,
|
||||||
database::delete_vault,
|
database::delete_vault,
|
||||||
@ -86,36 +79,13 @@ pub fn run() {
|
|||||||
database::vault_exists,
|
database::vault_exists,
|
||||||
extension::database::extension_sql_execute,
|
extension::database::extension_sql_execute,
|
||||||
extension::database::extension_sql_select,
|
extension::database::extension_sql_select,
|
||||||
//database::update_hlc_from_remote,
|
extension::get_all_extensions,
|
||||||
/* extension::copy_directory,
|
extension::get_extension_info,
|
||||||
extension::database::extension_sql_select, */
|
extension::install_extension_with_permissions,
|
||||||
|
extension::is_extension_installed,
|
||||||
|
extension::preview_extension,
|
||||||
|
extension::remove_extension,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.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"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<div class="absolute top-2 right-2">
|
<div class="absolute top-2 right-2">
|
||||||
<UiDropdown class="btn btn-sm btn-text btn-circle">
|
<UDropdownMenu>
|
||||||
<template #activator>
|
<UiButton icon="mdi:dots-vertical" />
|
||||||
<Icon name="mdi:dots-vertical" />
|
</UDropdownMenu>
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #items>
|
|
||||||
<UiButton
|
|
||||||
class="btn-error btn-outline btn-sm"
|
|
||||||
@click="showRemoveDialog = true"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:trash" /> {{ t('remove') }}
|
|
||||||
</UiButton>
|
|
||||||
</template>
|
|
||||||
</UiDropdown>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@ -44,6 +33,7 @@
|
|||||||
<div class="card-actions" v-if="$slots.action">
|
<div class="card-actions" v-if="$slots.action">
|
||||||
<slot name="action" />
|
<slot name="action" />
|
||||||
</div> -->
|
</div> -->
|
||||||
|
hier klicken
|
||||||
<div
|
<div
|
||||||
class="size-20 absolute bottom-2 right-2"
|
class="size-20 absolute bottom-2 right-2"
|
||||||
v-html="icon"
|
v-html="icon"
|
||||||
|
|||||||
@ -1,71 +1,107 @@
|
|||||||
<template>
|
<template>
|
||||||
<UiAccordion v-if="database?.read?.length">
|
<UAccordion v-if="database?.read?.length">
|
||||||
<template #title>
|
<template #title>
|
||||||
<h3>{{ t("permission.read") }}</h3>
|
<h3>{{ t('permission.read') }}</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ul class="space-y-0.5">
|
<ul class="space-y-0.5">
|
||||||
<li v-for="read in database?.read" class="flex items-center justify-between px-4 py-1">
|
<li
|
||||||
<div class="flex items-center gap-1">
|
v-for="read in database?.read"
|
||||||
<input :id="Object.keys(read).at(0)" type="checkbox" class="checkbox" :checked="Object.values(read).at(0)" >
|
class="flex items-center justify-between px-4 py-1"
|
||||||
<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">
|
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<input
|
<input
|
||||||
:id="Object.keys(write).at(0)" type="checkbox" class="checkbox"
|
:id="Object.keys(read).at(0)"
|
||||||
:checked="Object.values(write).at(0)" >
|
type="checkbox"
|
||||||
<label class="label-text text-base" :for="Object.keys(write).at(0)">{{ Object.keys(write).at(0) }}</label>
|
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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</UiAccordion>
|
</UAccordion>
|
||||||
|
|
||||||
<UiAccordion v-if="database?.create?.length">
|
<UAccordion v-if="database?.write?.length">
|
||||||
<template #title>
|
<template #title>
|
||||||
<h3>{{ t("permission.create") }}</h3>
|
<h3>{{ t('permission.write') }}</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ul class="space-y-0.5">
|
<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">
|
<div class="flex items-center gap-1">
|
||||||
<input
|
<input
|
||||||
:id="Object.keys(create).at(0)" type="checkbox" class="checkbox"
|
:id="Object.keys(write).at(0)"
|
||||||
:checked="Object.values(create).at(0)" >
|
type="checkbox"
|
||||||
<label class="label-text text-base" :for="Object.keys(create).at(0)">{{ Object.keys(create).at(0) }}</label>
|
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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
database?: {
|
||||||
defineProps<{ database?: { read?: Record<string, boolean>[], write?: Record<string, boolean>[], create?: Record<string, boolean>[] } }>();
|
read?: Record<string, boolean>[]
|
||||||
const { t } = useI18n();
|
write?: Record<string, boolean>[]
|
||||||
|
create?: Record<string, boolean>[]
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
const { t } = useI18n()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
de:
|
de:
|
||||||
permission:
|
permission:
|
||||||
read: Lesen
|
read: Lesen
|
||||||
write: Schreiben
|
write: Schreiben
|
||||||
create: Erstellen
|
create: Erstellen
|
||||||
|
|
||||||
en:
|
en:
|
||||||
permission:
|
permission:
|
||||||
read: Read
|
read: Read
|
||||||
write: Write
|
write: Write
|
||||||
create: Create
|
create: Create
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -1,38 +1,56 @@
|
|||||||
<template>
|
<template>
|
||||||
<UiAccordion v-if="filesystem?.read?.length">
|
<UAccordion v-if="filesystem?.read?.length">
|
||||||
<template #title>
|
<template #title>
|
||||||
<h3>{{ t('permission.read') }}</h3>
|
<h3>{{ t('permission.read') }}</h3>
|
||||||
</template>
|
</template>
|
||||||
<ul class="space-y-0.5">
|
<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">
|
<div class="flex items-center gap-1">
|
||||||
<input :id="Object.keys(read).at(0)" type="checkbox" class="checkbox" :checked="Object.values(read).at(0)" >
|
<input
|
||||||
<label class="label-text text-base" :for="Object.keys(read).at(0)">{{
|
:id="Object.keys(read).at(0)"
|
||||||
Object.keys(read).at(0)
|
type="checkbox"
|
||||||
}}</label>
|
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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</UiAccordion>
|
</UAccordion>
|
||||||
|
|
||||||
<UiAccordion v-if="filesystem?.write?.length">
|
<UAccordion v-if="filesystem?.write?.length">
|
||||||
<template #title>
|
<template #title>
|
||||||
<h3>{{ t('permission.write') }}</h3>
|
<h3>{{ t('permission.write') }}</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ul class="space-y-0.5">
|
<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">
|
<div class="flex items-center gap-1">
|
||||||
<input
|
<input
|
||||||
:id="Object.keys(write).at(0)" type="checkbox" class="checkbox"
|
:id="Object.keys(write).at(0)"
|
||||||
:checked="Object.values(write).at(0)" >
|
type="checkbox"
|
||||||
<label class="label-text text-base" :for="Object.keys(write).at(0)">{{
|
class="checkbox"
|
||||||
Object.keys(write).at(0)
|
:checked="Object.values(write).at(0)"
|
||||||
}}</label>
|
/>
|
||||||
|
<label
|
||||||
|
class="label-text text-base"
|
||||||
|
:for="Object.keys(write).at(0)"
|
||||||
|
>{{ Object.keys(write).at(0) }}</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</UiAccordion>
|
</UAccordion>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -46,13 +64,13 @@ const { t } = useI18n()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
de:
|
de:
|
||||||
permission:
|
permission:
|
||||||
read: Lesen
|
read: Lesen
|
||||||
write: Schreiben
|
write: Schreiben
|
||||||
|
|
||||||
en:
|
en:
|
||||||
permission:
|
permission:
|
||||||
read: Read
|
read: Read
|
||||||
write: Write
|
write: Write
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -1,33 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<UiAccordion>
|
<UAccordion>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h3>{{ t("http.access") }}</h3>
|
<h3>{{ t('http.access') }}</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ul class="space-y-0.5">
|
<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">
|
<div class="flex items-center gap-1">
|
||||||
<input
|
<input
|
||||||
:id="Object.keys(access).at(0)" type="checkbox" class="checkbox"
|
:id="Object.keys(access).at(0)"
|
||||||
:checked="Object.values(access).at(0)" >
|
type="checkbox"
|
||||||
<label class="label-text text-base" :for="Object.keys(access).at(0)">{{ Object.keys(access).at(0) }}</label>
|
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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</UiAccordion>
|
</UAccordion>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{ http?: Record<string, boolean>[] }>();
|
defineProps<{ http?: Record<string, boolean>[] }>()
|
||||||
const { t } = useI18n();
|
const { t } = useI18n()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
de:
|
de:
|
||||||
http:
|
http:
|
||||||
access: Internet Zugriff
|
access: Internet Zugriff
|
||||||
|
|
||||||
en:
|
en:
|
||||||
http:
|
http:
|
||||||
access: Internet Access
|
access: Internet Access
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
/**
|
// composables/extensionContextBroadcast.ts
|
||||||
* Broadcasts context changes to all active extensions
|
|
||||||
*/
|
|
||||||
export const useExtensionContextBroadcast = () => {
|
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) => {
|
const registerExtensionIframe = (iframe: HTMLIFrameElement) => {
|
||||||
extensionIframes.value.push(iframe)
|
extensionIframes.value.add(iframe)
|
||||||
}
|
}
|
||||||
|
|
||||||
const unregisterExtensionIframe = (iframe: HTMLIFrameElement) => {
|
const unregisterExtensionIframe = (iframe: HTMLIFrameElement) => {
|
||||||
extensionIframes.value = extensionIframes.value.filter((f) => f !== iframe)
|
extensionIframes.value.delete(iframe)
|
||||||
}
|
}
|
||||||
|
|
||||||
const broadcastContextChange = (context: {
|
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 {
|
interface ExtensionRequest {
|
||||||
id: string
|
id: string
|
||||||
@ -7,119 +9,124 @@ interface ExtensionRequest {
|
|||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExtensionResponse {
|
// Globaler Handler - nur einmal registriert
|
||||||
id: string
|
let globalHandlerRegistered = false
|
||||||
result?: unknown
|
const iframeRegistry = new Map<HTMLIFrameElement, IHaexHubExtension>()
|
||||||
error?: {
|
|
||||||
code: string
|
|
||||||
message: string
|
|
||||||
details?: unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useExtensionMessageHandler = (
|
const registerGlobalMessageHandler = () => {
|
||||||
iframeRef: Ref<HTMLIFrameElement | undefined | null>,
|
if (globalHandlerRegistered) return
|
||||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
|
||||||
) => {
|
window.addEventListener('message', async (event: MessageEvent) => {
|
||||||
const handleMessage = async (event: MessageEvent) => {
|
// Finde die Extension für dieses IFrame
|
||||||
// Security: Only accept messages from our iframe
|
let extension: IHaexHubExtension | undefined
|
||||||
if (!iframeRef.value || event.source !== iframeRef.value.contentWindow) {
|
let sourceIframe: HTMLIFrameElement | undefined
|
||||||
return
|
|
||||||
|
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
|
const request = event.data as ExtensionRequest
|
||||||
|
|
||||||
// Validate request structure
|
|
||||||
if (!request.id || !request.method) {
|
if (!request.id || !request.method) {
|
||||||
console.error('Invalid extension request:', request)
|
console.error('Invalid extension request:', request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[HaexHub] Extension request:', request.method, request.params)
|
console.log(
|
||||||
|
`[HaexHub] ${extension.name} request:`,
|
||||||
|
request.method,
|
||||||
|
request.params,
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result: unknown
|
let result: unknown
|
||||||
|
|
||||||
// Route request to appropriate handler
|
|
||||||
if (request.method.startsWith('extension.')) {
|
if (request.method.startsWith('extension.')) {
|
||||||
result = await handleExtensionMethod(request, extension)
|
result = await handleExtensionMethodAsync(request, extension)
|
||||||
} else if (request.method.startsWith('db.')) {
|
} 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.')) {
|
} else if (request.method.startsWith('permissions.')) {
|
||||||
result = await handlePermissionsMethod(request, extension)
|
result = await handlePermissionsMethodAsync(request, extension)
|
||||||
} else if (request.method.startsWith('context.')) {
|
} else if (request.method.startsWith('context.')) {
|
||||||
result = await handleContextMethod(request)
|
result = await handleContextMethodAsync(request)
|
||||||
} else if (request.method.startsWith('search.')) {
|
|
||||||
result = await handleSearchMethod(request, extension)
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown method: ${request.method}`)
|
throw new Error(`Unknown method: ${request.method}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send success response
|
sourceIframe.contentWindow?.postMessage(
|
||||||
sendResponse(iframeRef.value, {
|
{
|
||||||
id: request.id,
|
id: request.id,
|
||||||
result,
|
result,
|
||||||
})
|
},
|
||||||
|
'*',
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[HaexHub] Extension request error:', error)
|
console.error('[HaexHub] Extension request error:', error)
|
||||||
|
|
||||||
// Send error response
|
sourceIframe.contentWindow?.postMessage(
|
||||||
sendResponse(iframeRef.value, {
|
{
|
||||||
id: request.id,
|
id: request.id,
|
||||||
error: {
|
error: {
|
||||||
code: 'INTERNAL_ERROR',
|
code: 'INTERNAL_ERROR',
|
||||||
message: error instanceof Error ? error.message : 'Unknown error',
|
message: error instanceof Error ? error.message : 'Unknown error',
|
||||||
details: 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(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('message', handleMessage)
|
if (iframeRef.value) {
|
||||||
|
iframeRegistry.delete(iframeRef.value)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
|
||||||
handleMessage,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Extension Methods
|
// Extension Methods
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
async function handleExtensionMethod(
|
async function handleExtensionMethodAsync(
|
||||||
request: ExtensionRequest,
|
request: ExtensionRequest,
|
||||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
extension: IHaexHubExtension, // Direkter Typ, kein ComputedRef mehr
|
||||||
) {
|
) {
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'extension.getInfo':
|
case 'extension.getInfo':
|
||||||
return {
|
return await invoke('get_extension_info', {
|
||||||
keyHash: extension.value?.id || '', // TODO: Real key hash
|
extensionId: extension.id,
|
||||||
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 []
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown extension method: ${request.method}`)
|
throw new Error(`Unknown extension method: ${request.method}`)
|
||||||
}
|
}
|
||||||
@ -129,47 +136,41 @@ async function handleExtensionMethod(
|
|||||||
// Database Methods
|
// Database Methods
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
async function handleDatabaseMethod(
|
async function handleDatabaseMethodAsync(
|
||||||
request: ExtensionRequest,
|
request: ExtensionRequest,
|
||||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
extension: IHaexHubExtension, // Direkter Typ
|
||||||
) {
|
) {
|
||||||
const { currentVault } = useVaultStore()
|
const params = request.params as {
|
||||||
if (!currentVault) {
|
query?: string
|
||||||
throw new Error('No vault available')
|
params?: unknown[]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!extension.value) {
|
|
||||||
throw new Error('Extension not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = request.params as { query?: string; params?: unknown[] }
|
|
||||||
|
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'db.query': {
|
case 'db.query': {
|
||||||
// Validate permission
|
const rows = await invoke<unknown[]>('extension_sql_select', {
|
||||||
await validateDatabaseAccess(extension.value, params.query || '', 'read')
|
sql: params.query || '',
|
||||||
|
params: params.params || [],
|
||||||
// Execute query
|
extensionId: extension.id,
|
||||||
const result = await currentVault.drizzle.execute(params.query || '')
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: result.rows || [],
|
rows,
|
||||||
rowsAffected: 0,
|
rowsAffected: 0,
|
||||||
lastInsertId: undefined,
|
lastInsertId: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'db.execute': {
|
case 'db.execute': {
|
||||||
// Validate permission
|
await invoke<string[]>('extension_sql_execute', {
|
||||||
await validateDatabaseAccess(extension.value, params.query || '', 'write')
|
sql: params.query || '',
|
||||||
|
params: params.params || [],
|
||||||
// Execute query
|
extensionId: extension.id,
|
||||||
const result = await currentVault.drizzle.execute(params.query || '')
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: [],
|
rows: [],
|
||||||
rowsAffected: result.rowsAffected || 0,
|
rowsAffected: 1,
|
||||||
lastInsertId: result.lastInsertId,
|
lastInsertId: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,18 +178,14 @@ async function handleDatabaseMethod(
|
|||||||
const statements =
|
const statements =
|
||||||
(request.params as { statements?: string[] }).statements || []
|
(request.params as { statements?: string[] }).statements || []
|
||||||
|
|
||||||
// Validate all statements
|
|
||||||
for (const stmt of 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 }
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,125 +193,63 @@ async function handleDatabaseMethod(
|
|||||||
throw new Error(`Unknown database method: ${request.method}`)
|
throw new Error(`Unknown database method: ${request.method}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Permission Validation
|
// Filesystem Methods (TODO)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
async function validateDatabaseAccess(
|
async function handleFilesystemMethodAsync(
|
||||||
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(
|
|
||||||
request: ExtensionRequest,
|
request: ExtensionRequest,
|
||||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
extension: IHaexHubExtension,
|
||||||
) {
|
) {
|
||||||
switch (request.method) {
|
if (!request || !extension) return
|
||||||
case 'permissions.database.request': {
|
// TODO: Implementiere Filesystem Commands im Backend
|
||||||
const params = request.params as {
|
throw new Error('Filesystem methods not yet implemented')
|
||||||
resource: string
|
}
|
||||||
operation: 'read' | 'write'
|
|
||||||
reason?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Show user dialog to grant/deny permission
|
// ==========================================
|
||||||
console.log('[HaexHub] Permission request:', params)
|
// HTTP Methods (TODO)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
// For now: return ASK
|
async function handleHttpMethodAsync(
|
||||||
return {
|
request: ExtensionRequest,
|
||||||
status: 'ask',
|
extension: IHaexHubExtension,
|
||||||
permanent: false,
|
) {
|
||||||
}
|
if (!extension || !request) {
|
||||||
}
|
throw new Error('Extension not found')
|
||||||
|
|
||||||
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}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Context Methods
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
async function handleContextMethod(request: ExtensionRequest) {
|
async function handleContextMethodAsync(request: ExtensionRequest) {
|
||||||
const { theme } = useThemeStore()
|
const { currentTheme } = storeToRefs(useUiStore())
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
|
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'context.get':
|
case 'context.get':
|
||||||
return {
|
return {
|
||||||
theme: theme.value || 'system',
|
theme: currentTheme.value || 'system',
|
||||||
locale: locale.value,
|
locale: locale.value,
|
||||||
platform: detectPlatform(),
|
platform: detectPlatform(),
|
||||||
}
|
}
|
||||||
@ -330,29 +265,3 @@ function detectPlatform(): 'desktop' | 'mobile' | 'tablet' {
|
|||||||
if (width < 1024) return 'tablet'
|
if (width < 1024) return 'tablet'
|
||||||
return 'desktop'
|
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>
|
<template>
|
||||||
<UPage
|
<div
|
||||||
:ui="{
|
:ui="{
|
||||||
root: ['h-full w-full bg-elevated'],
|
root: ['h-full w-full bg-elevated lg:flex'],
|
||||||
center: ['h-full w-full'],
|
center: ['h-full w-full'],
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@ -34,7 +34,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</UiDialogConfirm>
|
</UiDialogConfirm>
|
||||||
</div>
|
</div>
|
||||||
</UPage>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@ -1,24 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-scroll">
|
<div class="h-full flex flex-col">
|
||||||
<div
|
<!-- Tab Bar -->
|
||||||
v-if="!iFrameSrc"
|
<div class="flex gap-2 p-2 bg-default overflow-x-auto border-b">
|
||||||
class="flex items-center justify-center h-full"
|
<div
|
||||||
>
|
v-for="tab in tabsStore.sortedTabs"
|
||||||
<p>{{ t('loading') }}</p>
|
: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>
|
</div>
|
||||||
<iframe
|
|
||||||
v-else
|
|
||||||
ref="iFrameRef"
|
|
||||||
class="w-full h-full"
|
|
||||||
:src="iFrameSrc"
|
|
||||||
sandbox="allow-scripts "
|
|
||||||
allow="autoplay; speaker-selection; encrypted-media;"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useExtensionMessageHandler } from '~/composables/extensionMessageHandler'
|
import { useExtensionMessageHandler } from '~/composables/extensionMessageHandler'
|
||||||
|
import { useExtensionTabsStore } from '~/stores/extensions/tabs'
|
||||||
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
name: 'haexExtension',
|
name: 'haexExtension',
|
||||||
@ -26,42 +67,84 @@ definePageMeta({
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const iFrameRef = useTemplateRef('iFrameRef')
|
const tabsStore = useExtensionTabsStore()
|
||||||
|
|
||||||
const { extensionEntry: iframeSrc, currentExtension } =
|
// Extension aus Route öffnen
|
||||||
storeToRefs(useExtensionsStore())
|
//const extensionId = computed(() => route.params.extensionId as string)
|
||||||
|
|
||||||
const iFrameSrc = computed(() =>
|
const { currentExtensionId } = storeToRefs(useExtensionsStore())
|
||||||
iframeSrc.value ? `${iframeSrc.value}/index.html` : '',
|
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 { currentTheme } = storeToRefs(useUiStore())
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
|
|
||||||
watch([currentTheme, locale], () => {
|
watch([currentTheme, locale], () => {
|
||||||
if (iFrameRef.value?.contentWindow) {
|
tabsStore.broadcastToAllTabs({
|
||||||
iFrameRef.value.contentWindow.postMessage(
|
type: 'context.changed',
|
||||||
{
|
data: {
|
||||||
type: 'context.changed',
|
context: {
|
||||||
data: {
|
theme: currentTheme.value || 'system',
|
||||||
context: {
|
locale: locale.value,
|
||||||
theme: currentTheme.value || 'system',
|
platform:
|
||||||
locale: locale.value,
|
window.innerWidth < 768
|
||||||
platform:
|
? 'mobile'
|
||||||
window.innerWidth < 768
|
: window.innerWidth < 1024
|
||||||
? 'mobile'
|
? 'tablet'
|
||||||
: window.innerWidth < 1024
|
: 'desktop',
|
||||||
? 'tablet'
|
|
||||||
: 'desktop',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
},
|
},
|
||||||
'*',
|
},
|
||||||
)
|
timestamp: Date.now(),
|
||||||
}
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cleanup beim Verlassen
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// Optional: Alle Tabs schließen oder offen lassen
|
||||||
|
// tabsStore.closeAllTabs()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -31,18 +31,17 @@
|
|||||||
class="size-full md:size-2/3 md:translate-x-1/5 md:translate-y-1/3"
|
class="size-full md:size-2/3 md:translate-x-1/5 md:translate-y-1/3"
|
||||||
/>
|
/>
|
||||||
<div class="fixed top-30 right-10">
|
<div class="fixed top-30 right-10">
|
||||||
<UiTooltip :tooltip="t('extension.add')">
|
<UiButton
|
||||||
<UiButton
|
class="btn-square btn-primary btn-xl btn-gradient rotate-45"
|
||||||
class="btn-square btn-primary btn-xl btn-gradient rotate-45"
|
:tooltip="t('extension.add')"
|
||||||
@click="prepareInstallExtensionAsync"
|
@click="prepareInstallExtensionAsync"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name="mdi:plus"
|
name="mdi:plus"
|
||||||
size="1.5em"
|
size="1.5em"
|
||||||
class="rotate-45"
|
class="rotate-45"
|
||||||
/>
|
/>
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</UiTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -93,7 +92,7 @@ const extension = reactive<{
|
|||||||
path: '',
|
path: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadExtensionManifestAsync = async () => {
|
/* const loadExtensionManifestAsync = async () => {
|
||||||
try {
|
try {
|
||||||
extension.path = await open({ directory: true, recursive: true })
|
extension.path = await open({ directory: true, recursive: true })
|
||||||
if (!extension.path) return
|
if (!extension.path) return
|
||||||
@ -111,7 +110,7 @@ const loadExtensionManifestAsync = async () => {
|
|||||||
add({ color: 'error', description: JSON.stringify(error) })
|
add({ color: 'error', description: JSON.stringify(error) })
|
||||||
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
|
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
|
|
||||||
const { add } = useToast()
|
const { add } = useToast()
|
||||||
const { addNotificationAsync } = useNotificationStore()
|
const { addNotificationAsync } = useNotificationStore()
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<UPage>
|
<div class="w-full">
|
||||||
<div class="h-screen bg-amber-300 flex-1 flex-wrap">
|
<div class="h-screen bg-amber-300 flex-1 flex-wrap">
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
</div>
|
</div>
|
||||||
<div class="h-screen bg-teal-300 flex-1">
|
<div class="h-screen bg-teal-300 flex-1">
|
||||||
abbbbbbbbbbbbbbbbbbbbb availableThemes:{{ uiStore.availableThemes }}
|
abbbbbbbbbbbbbbbbbbbbb availableThemes:{{ uiStore.availableThemes }}
|
||||||
</div>
|
</div>
|
||||||
</UPage>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@ -1,22 +1,44 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
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 {
|
import type {
|
||||||
IHaexHubExtension,
|
IHaexHubExtension,
|
||||||
IHaexHubExtensionLink,
|
|
||||||
IHaexHubExtensionManifest,
|
IHaexHubExtensionManifest,
|
||||||
} from '~/types/haexhub'
|
} from '~/types/haexhub'
|
||||||
import { haexExtensions } from '~~/src-tauri/database/schemas/vault'
|
|
||||||
|
|
||||||
const manifestFileName = 'manifest.json'
|
interface ExtensionInfoResponse {
|
||||||
const logoFileName = 'icon.svg'
|
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', () => {
|
export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||||
const availableExtensions = ref<IHaexHubExtensionLink[]>([])
|
const availableExtensions = ref<IHaexHubExtension[]>([])
|
||||||
const { addNotificationAsync } = useNotificationStore()
|
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
|
availableExtensions.value
|
||||||
.filter((extension) => extension.enabled && extension.installed)
|
.filter((extension) => extension.enabled && extension.installed)
|
||||||
.map((extension) => ({
|
.map((extension) => ({
|
||||||
@ -26,9 +48,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
|||||||
tooltip: extension.name ?? '',
|
tooltip: extension.name ?? '',
|
||||||
to: { name: 'haexExtension', params: { extensionId: extension.id } },
|
to: { name: 'haexExtension', params: { extensionId: extension.id } },
|
||||||
})),
|
})),
|
||||||
)
|
) */
|
||||||
|
|
||||||
const currentRoute = useRouter().currentRoute
|
|
||||||
|
|
||||||
const isActive = (id: string) =>
|
const isActive = (id: string) =>
|
||||||
computed(
|
computed(
|
||||||
@ -37,32 +57,26 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
|||||||
currentRoute.value.params.extensionId === id,
|
currentRoute.value.params.extensionId === id,
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentExtension = computed(() => {
|
const extensionEntry = computed(() => {
|
||||||
console.log('computed currentExtension', currentRoute.value.params)
|
if (!currentExtension.value?.version || !currentExtension.value?.id)
|
||||||
if (currentRoute.value.meta.name !== 'haexExtension') return
|
return null
|
||||||
|
|
||||||
const extensionId = getSingleRouteParam(
|
const encodedInfo = encodeExtensionInfo(
|
||||||
currentRoute.value.params.extensionId,
|
currentExtension.value.id,
|
||||||
|
currentExtension.value.version,
|
||||||
)
|
)
|
||||||
console.log('extensionId from param', extensionId)
|
return `extension://${encodedInfo}`
|
||||||
if (!extensionId) return
|
|
||||||
|
|
||||||
const extension = availableExtensions.value.find(
|
|
||||||
(extension) => extension.id === extensionId,
|
|
||||||
)
|
|
||||||
console.log('currentExtension', extension)
|
|
||||||
return extension
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const getExtensionPathAsync = async (
|
/* const getExtensionPathAsync = async (
|
||||||
extensionId?: string,
|
extensionId?: string,
|
||||||
version?: string,
|
version?: string,
|
||||||
) => {
|
) => {
|
||||||
if (!extensionId || !version) return ''
|
if (!extensionId || !version) return ''
|
||||||
return await join(await appDataDir(), 'extensions', extensionId, version)
|
return await join(await appDataDir(), 'extensions', extensionId, version)
|
||||||
}
|
} */
|
||||||
|
|
||||||
const checkSourceExtensionDirectoryAsync = async (
|
/* const checkSourceExtensionDirectoryAsync = async (
|
||||||
extensionDirectory: string,
|
extensionDirectory: string,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@ -82,22 +96,154 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
|||||||
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
||||||
//throw error //new Error(`Keine Leseberechtigung für Ordner ${extensionDirectory}`);
|
//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 (
|
/* const loadExtensionsAsync = async () => {
|
||||||
extension: Partial<IHaexHubExtension>,
|
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 {
|
try {
|
||||||
const extensionPath = await getExtensionPathAsync(
|
const extensionId = await invoke<string>('install_extension', {
|
||||||
extension.id,
|
sourcePath,
|
||||||
`${extension.version}`,
|
})
|
||||||
)
|
return extensionId
|
||||||
console.log(
|
|
||||||
`extension ${extension.id} is installed ${await exists(extensionPath)}`,
|
|
||||||
)
|
|
||||||
return await exists(extensionPath)
|
|
||||||
} catch (error) {
|
} 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
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,7 +302,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const readManifestFileAsync = async (
|
/* const readManifestFileAsync = async (
|
||||||
extensionId: string,
|
extensionId: string,
|
||||||
version: string,
|
version: string,
|
||||||
) => {
|
) => {
|
||||||
@ -173,173 +319,17 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
|||||||
await readTextFile(manifestPath),
|
await readTextFile(manifestPath),
|
||||||
)) as IHaexHubExtensionManifest
|
)) as IHaexHubExtensionManifest
|
||||||
|
|
||||||
/*
|
|
||||||
TODO implement check, that manifest has valid data
|
|
||||||
*/
|
|
||||||
return manifest
|
return manifest
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
||||||
console.error('ERROR readManifestFileAsync', error)
|
console.error('ERROR readManifestFileAsync', error)
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
|
|
||||||
const installAsync = async (extensionDirectory: string | null) => {
|
/* const extensionEntry = computedAsync(
|
||||||
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(
|
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
/* console.log("extensionEntry start", currentExtension.value);
|
|
||||||
const regex = /((href|src)=["'])([^"']+)(["'])/g; */
|
|
||||||
|
|
||||||
if (!currentExtension.value?.id || !currentExtension.value.version) {
|
if (!currentExtension.value?.id || !currentExtension.value.version) {
|
||||||
console.log('extension id or entry missing', currentExtension.value)
|
console.log('extension id or entry missing', currentExtension.value)
|
||||||
@ -375,60 +365,19 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
|||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
{ lazy: true },
|
{ 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 {
|
return {
|
||||||
availableExtensions,
|
availableExtensions,
|
||||||
checkManifest,
|
checkManifest,
|
||||||
currentExtension,
|
currentExtension,
|
||||||
|
currentExtensionId,
|
||||||
extensionEntry,
|
extensionEntry,
|
||||||
extensionLinks,
|
|
||||||
installAsync,
|
installAsync,
|
||||||
isActive,
|
isActive,
|
||||||
isExtensionInstalledAsync,
|
isExtensionInstalledAsync,
|
||||||
loadExtensionsAsync,
|
loadExtensionsAsync,
|
||||||
readManifestFileAsync,
|
|
||||||
removeExtensionAsync,
|
removeExtensionAsync,
|
||||||
getExtensionPathAsync,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -438,7 +387,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
|||||||
return 'text/plain'
|
return 'text/plain'
|
||||||
} */
|
} */
|
||||||
|
|
||||||
const removeExtensionFromVaultAsync = async (
|
/* const removeExtensionFromVaultAsync = async (
|
||||||
id: string | null,
|
id: string | null,
|
||||||
version: string | null,
|
version: string | null,
|
||||||
) => {
|
) => {
|
||||||
@ -457,9 +406,9 @@ const removeExtensionFromVaultAsync = async (
|
|||||||
.delete(haexExtensions)
|
.delete(haexExtensions)
|
||||||
.where(and(eq(haexExtensions.id, id), eq(haexExtensions.version, version)))
|
.where(and(eq(haexExtensions.id, id), eq(haexExtensions.version, version)))
|
||||||
return removedExtensions
|
return removedExtensions
|
||||||
}
|
} */
|
||||||
|
|
||||||
const removeExtensionFilesAsync = async (
|
/* const removeExtensionFilesAsync = async (
|
||||||
id: string | null,
|
id: string | null,
|
||||||
version: string | null,
|
version: string | null,
|
||||||
) => {
|
) => {
|
||||||
@ -483,4 +432,13 @@ const removeExtensionFilesAsync = async (
|
|||||||
console.error('ERROR removeExtensionFilesAsync', error)
|
console.error('ERROR removeExtensionFilesAsync', error)
|
||||||
throw new Error(JSON.stringify(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 {
|
export interface IHaexHubExtension {
|
||||||
author?: string | null
|
|
||||||
enabled?: boolean | null
|
|
||||||
icon?: string | null
|
|
||||||
id: string
|
id: string
|
||||||
manifest?: IHaexHubExtensionManifest
|
name: string
|
||||||
name: string | null
|
version: string
|
||||||
version?: string | null
|
author: string | null
|
||||||
|
icon: string | null
|
||||||
|
enabled: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user