refactore manifest and permission

This commit is contained in:
2025-10-02 01:42:30 +02:00
parent 56e75977cd
commit fb577a8699
51 changed files with 5634 additions and 2086 deletions

View File

@ -1,4 +1,4 @@
import tailwindcss from '@tailwindcss/vite'
//import tailwindcss from '@tailwindcss/vite'
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
@ -99,7 +99,7 @@ export default defineNuxtConfig({
},
vite: {
plugins: [tailwindcss()],
//plugins: [tailwindcss()],
// Better support for Tauri CLI output
clearScreen: false,
// Enable environment variables

479
src-tauri/Cargo.lock generated
View File

@ -17,6 +17,17 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@ -62,6 +73,15 @@ version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "ashpd"
version = "0.11.0"
@ -274,6 +294,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]]
name = "bincode"
version = "1.3.3"
@ -386,6 +412,15 @@ dependencies = [
"serde",
]
[[package]]
name = "bzip2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff"
dependencies = [
"libbz2-rs-sys",
]
[[package]]
name = "cairo-rs"
version = "0.18.5"
@ -440,7 +475,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"thiserror 2.0.16",
"thiserror 2.0.17",
]
[[package]]
@ -459,6 +494,8 @@ version = "1.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
dependencies = [
"jobserver",
"libc",
"shlex",
]
@ -514,6 +551,16 @@ dependencies = [
"windows-link",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "combine"
version = "4.6.7"
@ -533,6 +580,18 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "convert_case"
version = "0.4.0"
@ -627,6 +686,21 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.2"
@ -698,6 +772,33 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "darling"
version = "0.20.10"
@ -739,6 +840,22 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
[[package]]
name = "deflate64"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "deranged"
version = "0.4.0"
@ -749,6 +866,17 @@ dependencies = [
"serde",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "derive_more"
version = "0.99.19"
@ -770,6 +898,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
@ -890,6 +1019,30 @@ version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"serde",
"sha2",
"subtle",
"zeroize",
]
[[package]]
name = "embed-resource"
version = "3.0.2"
@ -1020,6 +1173,12 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "field-offset"
version = "0.3.6"
@ -1032,11 +1191,12 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.1.0"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"libz-rs-sys",
"miniz_oxide",
]
@ -1081,9 +1241,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
@ -1533,6 +1693,7 @@ name = "haex-hub"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"ed25519-dalek",
"fs_extra",
"hex",
"mime",
@ -1540,10 +1701,10 @@ dependencies = [
"rusqlite",
"serde",
"serde_json",
"sha2",
"sqlparser",
"tauri",
"tauri-build",
"tauri-plugin-android-fs",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-http",
@ -1552,11 +1713,12 @@ dependencies = [
"tauri-plugin-os",
"tauri-plugin-persisted-scope",
"tauri-plugin-store",
"thiserror 2.0.16",
"tokio",
"thiserror 2.0.17",
"ts-rs",
"uhlc",
"url",
"uuid",
"zip",
]
[[package]]
@ -1607,6 +1769,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "html5ever"
version = "0.29.1"
@ -1887,9 +2058,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
@ -1937,6 +2108,15 @@ dependencies = [
"cfb",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "io-uring"
version = "0.7.8"
@ -2024,6 +2204,16 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.2",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.77"
@ -2109,6 +2299,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "libbz2-rs-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
[[package]]
name = "libc"
version = "0.2.175"
@ -2147,6 +2343,15 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "libz-rs-sys"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd"
dependencies = [
"zlib-rs",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@ -2187,6 +2392,16 @@ version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "lzma-rust2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a"
dependencies = [
"crc",
"sha2",
]
[[package]]
name = "mac"
version = "0.1.1"
@ -2305,7 +2520,7 @@ dependencies = [
"once_cell",
"png",
"serde",
"thiserror 2.0.16",
"thiserror 2.0.17",
"windows-sys 0.59.0",
]
@ -2775,10 +2990,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "percent-encoding"
version = "2.3.1"
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
@ -2937,6 +3162,16 @@ dependencies = [
"futures-io",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
@ -2990,6 +3225,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppmd-rust"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@ -3129,7 +3370,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2 0.5.8",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"tracing",
"web-time",
@ -3149,7 +3390,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tinyvec",
"tracing",
"web-time",
@ -3338,7 +3579,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.15",
"libredox",
"thiserror 2.0.16",
"thiserror 2.0.17",
]
[[package]]
@ -3646,9 +3887,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.226"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
@ -3667,18 +3908,18 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.226"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.226"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@ -3813,10 +4054,21 @@ dependencies = [
]
[[package]]
name = "sha2"
version = "0.10.8"
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
@ -3838,6 +4090,15 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
@ -3946,10 +4207,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
[[package]]
name = "sqlparser"
version = "0.58.0"
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec4b661c54b1e4b603b37873a18c59920e4c51ea8ea2cf527d925424dbd4437c"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "sqlparser"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4591acadbcf52f0af60eafbb2c003232b2b4cd8de5f0e9437cb8b1b59046cc0f"
dependencies = [
"log",
"recursive",
@ -4224,7 +4495,7 @@ dependencies = [
"tauri-runtime",
"tauri-runtime-wry",
"tauri-utils",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"tray-icon",
"url",
@ -4277,7 +4548,7 @@ dependencies = [
"sha2",
"syn 2.0.100",
"tauri-utils",
"thiserror 2.0.16",
"thiserror 2.0.17",
"time",
"url",
"uuid",
@ -4315,21 +4586,6 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-android-fs"
version = "12.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1387b55109ae9b8ad0521ac11f8ce827740f53c0e0ce74648d1cb2efe0fd9c09"
dependencies = [
"percent-encoding",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.16",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.4.0"
@ -4344,7 +4600,7 @@ dependencies = [
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.16",
"thiserror 2.0.17",
"url",
]
@ -4365,7 +4621,7 @@ dependencies = [
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.16",
"thiserror 2.0.17",
"toml 0.9.5",
"url",
]
@ -4388,7 +4644,7 @@ dependencies = [
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"url",
"urlpattern",
@ -4408,7 +4664,7 @@ dependencies = [
"serde_repr",
"tauri",
"tauri-plugin",
"thiserror 2.0.16",
"thiserror 2.0.17",
"time",
"url",
]
@ -4429,7 +4685,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.16",
"thiserror 2.0.17",
"url",
"windows",
"zbus",
@ -4450,7 +4706,7 @@ dependencies = [
"sys-locale",
"tauri",
"tauri-plugin",
"thiserror 2.0.16",
"thiserror 2.0.17",
]
[[package]]
@ -4466,7 +4722,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin-fs",
"thiserror 2.0.16",
"thiserror 2.0.17",
]
[[package]]
@ -4480,7 +4736,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"tracing",
]
@ -4503,7 +4759,7 @@ dependencies = [
"serde",
"serde_json",
"tauri-utils",
"thiserror 2.0.16",
"thiserror 2.0.17",
"url",
"webkit2gtk",
"webview2-com",
@ -4567,7 +4823,7 @@ dependencies = [
"serde_json",
"serde_with",
"swift-rs",
"thiserror 2.0.16",
"thiserror 2.0.17",
"toml 0.9.5",
"url",
"urlpattern",
@ -4592,7 +4848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
dependencies = [
"quick-xml 0.37.5",
"thiserror 2.0.16",
"thiserror 2.0.17",
"windows",
"windows-version",
]
@ -4641,11 +4897,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.16"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl 2.0.16",
"thiserror-impl 2.0.17",
]
[[package]]
@ -4661,9 +4917,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.16"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
@ -4951,7 +5207,7 @@ dependencies = [
"once_cell",
"png",
"serde",
"thiserror 2.0.16",
"thiserror 2.0.17",
"windows-sys 0.59.0",
]
@ -4967,7 +5223,7 @@ version = "11.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be"
dependencies = [
"thiserror 2.0.16",
"thiserror 2.0.17",
"ts-rs-macros",
]
@ -5087,9 +5343,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.4"
version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
@ -5405,7 +5661,7 @@ version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
dependencies = [
"thiserror 2.0.16",
"thiserror 2.0.17",
"windows",
"windows-core 0.61.0",
]
@ -5965,7 +6221,7 @@ dependencies = [
"sha2",
"soup3",
"tao-macros",
"thiserror 2.0.16",
"thiserror 2.0.17",
"url",
"webkit2gtk",
"webkit2gtk-sys",
@ -6128,6 +6384,20 @@ name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "zerovec"
@ -6151,6 +6421,79 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "zip"
version = "5.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532"
dependencies = [
"aes",
"arbitrary",
"bzip2",
"constant_time_eq",
"crc32fast",
"deflate64",
"flate2",
"getrandom 0.3.2",
"hmac",
"indexmap 2.8.0",
"lzma-rust2",
"memchr",
"pbkdf2",
"ppmd-rust",
"sha1",
"time",
"zeroize",
"zopfli",
"zstd",
]
[[package]]
name = "zlib-rs"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2"
[[package]]
name = "zopfli"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "zvariant"
version = "5.7.0"

View File

@ -18,37 +18,38 @@ crate-type = ["staticlib", "cdylib", "rlib"]
serde_json = "1.0.145"
tauri-build = { version = "2.2", features = [] }
serde = { version = "1.0.226", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] }
[dependencies]
rusqlite = { version = "0.37.0", features = [
"load_extension",
"bundled-sqlcipher-vendored-openssl",
"functions",
] }
#libsqlite3-sys = { version = "0.31", features = ["bundled-sqlcipher"] }
#tauri-plugin-sql = { version = "2", features = ["sqlite"] }tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }#libsqlite3-sys = { version = "0.31", features = ["bundled-sqlcipher"] }
#sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] }
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
serde = { version = "1", features = ["derive"] }
hex = "0.4"
serde_json = "1.0.143"
base64 = "0.22"
mime_guess = "2.0"
mime = "0.3"
ed25519-dalek = "2.1"
fs_extra = "1.3.0"
sqlparser = { version = "0.58.0", features = ["visitor"] }
uhlc = "0.8"
hex = "0.4"
mime = "0.3"
mime_guess = "2.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.143"
sha2 = "0.10.9"
sqlparser = { version = "0.59.0", features = ["visitor"] }
tauri = { version = "2.8.5", features = ["protocol-asset", "devtools"] }
tauri-plugin-dialog = "2.4.0"
tauri-plugin-fs = "2.4.0"
tauri-plugin-opener = "2.5.0"
tauri-plugin-os = "2.3"
tauri-plugin-store = "2.4.0"
tauri-plugin-http = "2.5.2"
tauri-plugin-notification = "2.3.1"
tauri-plugin-opener = "2.5.0"
tauri-plugin-os = "2.3"
tauri-plugin-persisted-scope = "2.3.2"
tauri-plugin-android-fs = "12.0.1"
uuid = { version = "1.18.1", features = ["v4"] }
tauri-plugin-store = "2.4.0"
thiserror = "2.0.17"
ts-rs = "11.0.1"
thiserror = "2.0.16"
#tauri-plugin-sql = { version = "2", features = ["sqlite"] }
uhlc = "0.8"
uuid = { version = "1.18.1", features = ["v4"] }
zip = "5.1.1"
url = "2.5.7"

View File

@ -25,7 +25,6 @@
"fs:allow-download-read-recursive",
"fs:allow-download-write-recursive",
"fs:default",
"android-fs:default",
{
"identifier": "fs:scope",
"allow": [{ "path": "**" }]

View File

@ -1,5 +1,5 @@
import { drizzle } from 'drizzle-orm/sqlite-proxy' // Adapter für Query Building ohne direkte Verbindung
import * as schema from './schemas/vault' // Importiere alles aus deiner Schema-Datei
import * as schema from './schemas/haex' // Importiere alles aus deiner Schema-Datei
// sqlite-proxy benötigt eine (dummy) Ausführungsfunktion als Argument.
// Diese wird in unserem Tauri-Workflow nie aufgerufen, da wir nur .toSQL() verwenden.

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

View 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": {}
}
}

View File

@ -85,6 +85,13 @@
"when": 1757968140525,
"tag": "0011_illegal_thor_girl",
"breakpoints": true
},
{
"idx": 12,
"version": "6",
"when": 1759362109283,
"tag": "0012_special_gwen_stacy",
"breakpoints": true
}
]
}

View 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

View File

@ -4,59 +4,10 @@ import {
primaryKey,
sqliteTable,
text,
unique,
type AnySQLiteColumn,
} from 'drizzle-orm/sqlite-core'
import tableNames from '../tableNames.json'
export const haexSettings = sqliteTable(tableNames.haex.settings, {
id: text().primaryKey(),
key: text(),
type: text(),
value: text(),
haex_tombstone: integer({ mode: 'boolean' }),
})
export type InsertHaexSettings = typeof haexSettings.$inferInsert
export type SelectHaexSettings = typeof haexSettings.$inferSelect
export const haexExtensions = sqliteTable(tableNames.haex.extensions, {
id: text().primaryKey(),
author: text(),
enabled: integer({ mode: 'boolean' }),
icon: text(),
name: text(),
url: text(),
version: text(),
haex_tombstone: integer({ mode: 'boolean' }),
})
export type InsertHaexExtensions = typeof haexExtensions.$inferInsert
export type SelectHaexExtensions = typeof haexExtensions.$inferSelect
export const haexExtensionPermissions = sqliteTable(
tableNames.haex.extension_permissions,
{
id: text().primaryKey(),
extensionId: text('extension_id').references(
(): AnySQLiteColumn => haexExtensions.id,
),
resource: text({ enum: ['fs', 'http', 'db', 'shell'] }),
operation: text({ enum: ['read', 'write', 'create'] }),
path: text(),
createdAt: text('created_at').default(sql`(CURRENT_TIMESTAMP)`),
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
() => new Date(),
),
haex_tombstone: integer({ mode: 'boolean' }),
},
(table) => [
unique().on(table.extensionId, table.resource, table.operation, table.path),
],
)
export type InserthaexExtensionPermissions =
typeof haexExtensionPermissions.$inferInsert
export type SelecthaexExtensionPermissions =
typeof haexExtensionPermissions.$inferSelect
export const haexNotifications = sqliteTable(tableNames.haex.notifications, {
id: text().primaryKey(),
alt: text(),

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -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"]}}

View File

@ -2270,12 +2270,6 @@
"Identifier": {
"description": "Permission identifier",
"oneOf": [
{
"description": "Default permissions for the plugin",
"type": "string",
"const": "android-fs:default",
"markdownDescription": "Default permissions for the plugin"
},
{
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
"type": "string",

View File

@ -2270,12 +2270,6 @@
"Identifier": {
"description": "Permission identifier",
"oneOf": [
{
"description": "Default permissions for the plugin",
"type": "string",
"const": "android-fs:default",
"markdownDescription": "Default permissions for the plugin"
},
{
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
"type": "string",

View File

@ -10,8 +10,8 @@ use std::{
sync::{Arc, Mutex},
time::Duration,
};
use tauri::{AppHandle, Wry};
use tauri_plugin_store::{Store, StoreExt};
use tauri::AppHandle;
use tauri_plugin_store::StoreExt;
use thiserror::Error;
use uhlc::{HLCBuilder, Timestamp, HLC, ID};
use uuid::Uuid;

View File

@ -755,6 +755,7 @@ impl CrdtTransformer {
selection: del_stmt.selection.clone(),
returning: None,
or: None,
limit: None,
};
}
Ok(())

View File

@ -1,3 +1,4 @@
// src-tauri/src/crdt/trigger.rs
use crate::table_names::TABLE_CRDT_LOGS;
use rusqlite::{Connection, Result as RusqliteResult, Row, Transaction};
use serde::Serialize;
@ -11,7 +12,7 @@ const UPDATE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_update";
//const SYNC_ACTIVE_KEY: &str = "sync_active";
pub const TOMBSTONE_COLUMN: &str = "haex_tombstone";
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_hlc_timestamp";
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_timestamp";
#[derive(Debug)]
pub enum CrdtSetupError {

View File

@ -1,7 +1,5 @@
// src-tauri/src/database/core.rs
use std::collections::HashMap;
use crate::database::error::DatabaseError;
use crate::database::DbConnection;
use base64::{engine::general_purpose::STANDARD, Engine as _};
@ -14,6 +12,7 @@ use serde_json::Value as JsonValue;
use sqlparser::ast::{Query, Select, SetExpr, Statement, TableFactor, TableObject};
use sqlparser::dialect::SQLiteDialect;
use sqlparser::parser::Parser;
use std::collections::HashMap;
/// Öffnet und initialisiert eine Datenbank mit Verschlüsselung
pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connection, DatabaseError> {
@ -376,6 +375,7 @@ fn extract_tables_from_set_expr_recursive(set_expr: &SetExpr, tables: &mut Vec<S
| SetExpr::Table(_)
| SetExpr::Insert(_)
| SetExpr::Update(_)
| SetExpr::Merge(_)
| SetExpr::Delete(_) => {}
}
}

View File

@ -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)
}

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

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

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

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

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

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

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

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

View File

@ -1,14 +1,15 @@
// src-tauri/src/extension/database/mod.rs
pub mod permissions;
pub mod executor;
use crate::crdt::hlc::HlcService;
use crate::crdt::transformer::CrdtTransformer;
use crate::crdt::trigger;
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
use crate::database::error::DatabaseError;
use crate::extension::error::ExtensionError;
use crate::extension::permissions::validator::SqlPermissionValidator;
use crate::AppState;
use permissions::{check_read_permission, check_write_permission};
use rusqlite::params_from_iter;
use rusqlite::types::Value as SqlValue;
use rusqlite::Transaction;
@ -116,7 +117,7 @@ pub async fn extension_sql_execute(
hlc_service: State<'_, HlcService>,
) -> Result<Vec<String>, ExtensionError> {
// Permission check
check_write_permission(&state.db, &extension_id, sql).await?;
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
// Parameter validation
validate_params(sql, &params)?;
@ -186,7 +187,7 @@ pub async fn extension_sql_select(
state: State<'_, AppState>,
) -> Result<Vec<JsonValue>, ExtensionError> {
// Permission check
check_read_permission(&state.db, &extension_id, sql).await?;
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
// Parameter validation
validate_params(sql, &params)?;

View File

@ -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"),
}
} */
}

View File

@ -1,9 +1,36 @@
/// src-tauri/src/extension/error.rs
// src-tauri/src/extension/error.rs
use thiserror::Error;
use crate::database::error::DatabaseError;
/// Comprehensive error type for extension operations
/// Error codes for frontend handling
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExtensionErrorCode {
SecurityViolation = 1000,
NotFound = 1001,
PermissionDenied = 1002,
Database = 2000,
Filesystem = 2001,
Http = 2002,
Shell = 2003,
Manifest = 3000,
Validation = 3001,
InvalidPublicKey = 4000,
InvalidSignature = 4001,
SignatureVerificationFailed = 4002,
CalculateHash = 4003,
Installation = 5000,
}
impl serde::Serialize for ExtensionErrorCode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u16(*self as u16)
}
}
#[derive(Error, Debug)]
pub enum ExtensionError {
#[error("Security violation: {reason}")]
@ -29,15 +56,10 @@ pub enum ExtensionError {
Filesystem {
#[from]
source: std::io::Error,
// oder: source: FilesystemError,
},
#[error("HTTP request failed: {reason}")]
Http {
reason: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
Http { reason: String },
#[error("Shell command failed: {reason}")]
Shell {
@ -45,29 +67,51 @@ pub enum ExtensionError {
exit_code: Option<i32>,
},
/* #[error("IO error: {source}")]
Io {
#[from]
source: std::io::Error,
}, */
#[error("Manifest error: {reason}")]
ManifestError { reason: String },
#[error("Validation error: {reason}")]
ValidationError { reason: String },
#[error("Dev server error: {reason}")]
DevServerError { reason: String },
#[error("Invalid Public Key: {reason}")]
InvalidPublicKey { reason: String },
#[error("Serialization error: {reason}")]
SerializationError { reason: String },
#[error("Invalid Signature: {reason}")]
InvalidSignature { reason: String },
#[error("Configuration error: {reason}")]
ConfigError { reason: String },
#[error("Error during hash calculation: {reason}")]
CalculateHashError { reason: String },
#[error("Signature verification failed: {reason}")]
SignatureVerificationFailed { reason: String },
#[error("Extension installation failed: {reason}")]
InstallationFailed { reason: String },
}
impl ExtensionError {
/// Convenience constructor for permission denied errors
/// Get error code for this error
pub fn code(&self) -> ExtensionErrorCode {
match self {
ExtensionError::SecurityViolation { .. } => ExtensionErrorCode::SecurityViolation,
ExtensionError::NotFound { .. } => ExtensionErrorCode::NotFound,
ExtensionError::PermissionDenied { .. } => ExtensionErrorCode::PermissionDenied,
ExtensionError::Database { .. } => ExtensionErrorCode::Database,
ExtensionError::Filesystem { .. } => ExtensionErrorCode::Filesystem,
ExtensionError::Http { .. } => ExtensionErrorCode::Http,
ExtensionError::Shell { .. } => ExtensionErrorCode::Shell,
ExtensionError::ManifestError { .. } => ExtensionErrorCode::Manifest,
ExtensionError::ValidationError { .. } => ExtensionErrorCode::Validation,
ExtensionError::InvalidPublicKey { .. } => ExtensionErrorCode::InvalidPublicKey,
ExtensionError::InvalidSignature { .. } => ExtensionErrorCode::InvalidSignature,
ExtensionError::SignatureVerificationFailed { .. } => {
ExtensionErrorCode::SignatureVerificationFailed
}
ExtensionError::InstallationFailed { .. } => ExtensionErrorCode::Installation,
ExtensionError::CalculateHashError { .. } => ExtensionErrorCode::CalculateHash,
}
}
pub fn permission_denied(extension_id: &str, operation: &str, resource: &str) -> Self {
Self::PermissionDenied {
extension_id: extension_id.to_string(),
@ -76,34 +120,6 @@ impl ExtensionError {
}
}
/// Convenience constructor for HTTP errors
pub fn http_error(reason: &str) -> Self {
Self::Http {
reason: reason.to_string(),
source: None,
}
}
/// Convenience constructor for HTTP errors with source
pub fn http_error_with_source(
reason: &str,
source: Box<dyn std::error::Error + Send + Sync>,
) -> Self {
Self::Http {
reason: reason.to_string(),
source: Some(source),
}
}
/// Convenience constructor for shell errors
pub fn shell_error(reason: &str, exit_code: Option<i32>) -> Self {
Self::Shell {
reason: reason.to_string(),
exit_code,
}
}
/// Check if this error is related to permissions
pub fn is_permission_error(&self) -> bool {
matches!(
self,
@ -111,11 +127,9 @@ impl ExtensionError {
)
}
/// Extract extension ID if available
pub fn extension_id(&self) -> Option<&str> {
match self {
ExtensionError::PermissionDenied { extension_id, .. } => Some(extension_id),
ExtensionError::Database { source } => source.extension_id(),
_ => None,
}
}
@ -128,29 +142,12 @@ impl serde::Serialize for ExtensionError {
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("ExtensionError", 3)?;
let mut state = serializer.serialize_struct("ExtensionError", 4)?;
// Error type as discriminator
let error_type = match self {
ExtensionError::SecurityViolation { .. } => "SecurityViolation",
ExtensionError::NotFound { .. } => "NotFound",
ExtensionError::PermissionDenied { .. } => "PermissionDenied",
ExtensionError::Database { .. } => "Database",
ExtensionError::Filesystem { .. } => "Filesystem",
ExtensionError::Http { .. } => "Http",
ExtensionError::Shell { .. } => "Shell",
//ExtensionError::Io { .. } => "Io",
ExtensionError::ManifestError { .. } => "ManifestError",
ExtensionError::ValidationError { .. } => "ValidationError",
ExtensionError::DevServerError { .. } => "DevServerError",
ExtensionError::SerializationError { .. } => "SerializationError",
ExtensionError::ConfigError { .. } => "ConfigError",
};
state.serialize_field("type", error_type)?;
state.serialize_field("code", &self.code())?;
state.serialize_field("type", &format!("{:?}", self))?;
state.serialize_field("message", &self.to_string())?;
// Add extension_id if available
if let Some(ext_id) = self.extension_id() {
state.serialize_field("extension_id", ext_id)?;
} else {
@ -161,54 +158,16 @@ impl serde::Serialize for ExtensionError {
}
}
// For Tauri command serialization
impl From<ExtensionError> for String {
fn from(error: ExtensionError) -> Self {
serde_json::to_string(&error).unwrap_or_else(|_| error.to_string())
}
}
impl From<serde_json::Error> for ExtensionError {
fn from(err: serde_json::Error) -> Self {
ExtensionError::SerializationError {
ExtensionError::ManifestError {
reason: err.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::database::error::DatabaseError;
/* #[test]
fn test_database_error_conversion() {
let db_error = DatabaseError::access_denied("ext1", "read", "users", "no permission");
let ext_error: ExtensionError = db_error.into();
assert!(ext_error.is_permission_error());
assert_eq!(ext_error.extension_id(), Some("ext1"));
} */
#[test]
fn test_permission_denied_constructor() {
let error = ExtensionError::permission_denied("ext1", "write", "config.json");
match error {
ExtensionError::PermissionDenied {
extension_id,
operation,
resource,
} => {
assert_eq!(extension_id, "ext1");
assert_eq!(operation, "write");
assert_eq!(resource, "config.json");
}
_ => panic!("Expected PermissionDenied error"),
}
}
#[test]
fn test_serialization() {
let error = ExtensionError::permission_denied("ext1", "read", "database");
let serialized = serde_json::to_string(&error).unwrap();
// Basic check that it serializes properly
assert!(serialized.contains("PermissionDenied"));
assert!(serialized.contains("ext1"));
}
}

View File

@ -1,19 +1,184 @@
use crate::extension::core::{ExtensionInfoResponse, ExtensionManager};
use tauri::State;
/// src-tauri/src/extension/mod.rs
use crate::{
extension::{
core::{EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
error::ExtensionError,
},
AppState,
};
use tauri::{AppHandle, State};
pub mod core;
pub mod crypto;
pub mod database;
pub mod error;
pub mod filesystem;
pub mod permission_manager;
pub mod permissions;
#[tauri::command]
pub fn get_extension_info(
extension_id: String,
extension_manager: State<ExtensionManager>,
state: State<AppState>,
) -> Result<ExtensionInfoResponse, String> {
let extension = extension_manager
let extension = state
.extension_manager
.get_extension(&extension_id)
.ok_or_else(|| format!("Extension nicht gefunden: {}", extension_id))?;
Ok(ExtensionInfoResponse::from_extension(&extension))
ExtensionInfoResponse::from_extension(&extension).map_err(|e| format!("{:?}", e))
}
#[tauri::command]
pub fn get_all_extensions(state: State<AppState>) -> Result<Vec<ExtensionInfoResponse>, String> {
let mut extensions = Vec::new();
// Production Extensions
{
let prod_exts = state
.extension_manager
.production_extensions
.lock()
.unwrap();
for ext in prod_exts.values() {
extensions.push(ExtensionInfoResponse::from_extension(ext)?);
}
}
// Dev Extensions
{
let dev_exts = state.extension_manager.dev_extensions.lock().unwrap();
for ext in dev_exts.values() {
extensions.push(ExtensionInfoResponse::from_extension(ext)?);
}
}
Ok(extensions)
}
#[tauri::command]
pub async fn preview_extension(
state: State<'_, AppState>,
source_path: String,
) -> Result<ExtensionPreview, ExtensionError> {
state
.extension_manager
.preview_extension_internal(source_path)
.await
}
#[tauri::command]
pub async fn install_extension_with_permissions(
app_handle: AppHandle,
source_path: String,
custom_permissions: EditablePermissions,
state: State<'_, AppState>,
) -> Result<String, ExtensionError> {
state
.extension_manager
.install_extension_with_permissions_internal(
app_handle,
source_path,
custom_permissions,
&state,
)
.await
}
/* #[tauri::command]
pub async fn install_extension(
app_handle: AppHandle,
source_path: String,
state: State<'_, AppState>,
) -> Result<String, String> {
let source = PathBuf::from(&source_path);
// Manifest laden
let manifest_path = source.join("manifest.json");
let manifest_content = std::fs::read_to_string(&manifest_path)
.map_err(|e| format!("Manifest konnte nicht gelesen werden: {}", e))?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)
.map_err(|e| format!("Manifest ist ungültig: {}", e))?;
// Signatur verifizieren
let content_hash = ExtensionCrypto::hash_directory(&source)?;
ExtensionCrypto::verify_signature(&manifest.public_key, &content_hash, &manifest.signature)?;
// Key Hash berechnen
let key_hash = manifest.calculate_key_hash()?;
let full_extension_id = format!("{}-{}", key_hash, manifest.id);
// Zielverzeichnis mit Key Hash Prefix
let extensions_dir = app_handle
.path()
.app_data_dir()
.map_err(|e| format!("App-Datenverzeichnis nicht gefunden: {}", e))?
.join("extensions")
.join(&full_extension_id) // <- z.B. "a3f5b9c2d1e8f4-haex-pass"
.join(&manifest.version);
// Extension-Dateien kopieren
std::fs::create_dir_all(&extensions_dir)
.map_err(|e| format!("Verzeichnis konnte nicht erstellt werden: {}", e))?;
let source_to_copy = if source.join("dist").exists() {
source.join("dist") // Kopiere aus dist/
} else {
source.clone() // Kopiere direkt
};
copy_directory(
source_to_copy.to_string_lossy().to_string(),
extensions_dir.to_string_lossy().to_string(),
)?;
// Permissions speichern
let permissions = manifest.to_internal_permissions();
PermissionManager::save_permissions(&state.db, &permissions)
.await
.map_err(|e| format!("Fehler beim Speichern der Permissions: {:?}", e))?;
// Extension registrieren
let extension = Extension {
id: full_extension_id.clone(),
name: manifest.name.clone(),
source: ExtensionSource::Production {
path: extensions_dir.clone(),
version: manifest.version.clone(),
},
manifest: manifest.clone(),
enabled: true,
last_accessed: SystemTime::now(),
};
state
.extension_manager
.add_production_extension(extension)
.map_err(|e| format!("Extension konnte nicht hinzugefügt werden: {:?}", e))?;
Ok(full_extension_id)
}
*/
#[tauri::command]
pub async fn remove_extension(
app_handle: AppHandle,
extension_id: String,
extension_version: String,
state: State<'_, AppState>,
) -> Result<(), ExtensionError> {
state
.extension_manager
.remove_extension_internal(&app_handle, extension_id, extension_version, &state)
.await
}
#[tauri::command]
pub fn is_extension_installed(
extension_id: String,
extension_version: String,
state: State<'_, AppState>,
) -> Result<bool, String> {
if let Some(ext) = state.extension_manager.get_extension(&extension_id) {
Ok(ext.manifest.version == extension_version)
} else {
Ok(false)
}
}

View File

@ -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"));
}
}

View 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, &params)?;
}
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, &params)?;
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, &params)?;
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, &params)?;
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, &params)?;
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, &params)?;
// 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())
}
}

View File

@ -0,0 +1,3 @@
pub mod manager;
pub mod types;
pub mod validator;

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

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

View File

@ -1,28 +1,22 @@
//mod browser;
//mod android_storage;
mod crdt;
mod database;
mod extension;
//mod models;
use crate::{
crdt::hlc::HlcService,
database::DbConnection,
extension::core::{ExtensionManager, ExtensionState},
};
use std::sync::{Arc, Mutex};
use tauri::Manager;
pub mod table_names {
include!(concat!(env!("OUT_DIR"), "/tableNames.rs"));
}
use std::sync::{Arc, Mutex};
use crate::{crdt::hlc::HlcService, database::DbConnection, extension::core::ExtensionState};
/* use crate::{
crdt::hlc::HlcService,
database::{AppState, DbConnection},
extension::core::ExtensionState,
}; */
pub struct AppState {
pub db: DbConnection,
pub hlc: Mutex<HlcService>, // Kein Arc hier nötig, da der ganze AppState von Tauri in einem Arc verwaltet wird.
pub hlc: Mutex<HlcService>,
pub extension_manager: ExtensionManager,
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@ -31,26 +25,27 @@ pub fn run() {
tauri::Builder::default()
.register_uri_scheme_protocol(protocol_name, move |context, request| {
match extension::core::extension_protocol_handler(&context, &request) {
Ok(response) => response, // Wenn der Handler Ok ist, gib die Response direkt zurück
// Hole den AppState aus dem Context
let app_handle = context.app_handle();
let state = app_handle.state::<AppState>();
// Rufe den Handler mit allen benötigten Parametern auf
match extension::core::extension_protocol_handler(state, &app_handle, &request) {
Ok(response) => response,
Err(e) => {
// Wenn der Handler einen Fehler zurückgibt, logge ihn und erstelle eine Fehler-Response
eprintln!(
"Fehler im Custom Protocol Handler für URI '{}': {}",
request.uri(),
e
);
// Erstelle eine HTTP 500 Fehler-Response
// Du kannst hier auch spezifischere Fehler-Responses bauen, falls gewünscht.
tauri::http::Response::builder()
.status(500)
.header("Content-Type", "text/plain") // Optional, aber gut für Klarheit
.header("Content-Type", "text/plain")
.body(Vec::from(format!(
"Interner Serverfehler im Protokollhandler: {}",
e
)))
.unwrap_or_else(|build_err| {
// Fallback, falls selbst das Erstellen der Fehler-Response fehlschlägt
eprintln!("Konnte Fehler-Response nicht erstellen: {}", build_err);
tauri::http::Response::builder()
.status(500)
@ -60,11 +55,10 @@ pub fn run() {
}
}
})
/* .manage(database::DbConnection(Arc::new(Mutex::new(None))))
.manage(crdt::hlc::HlcService::new()) */
.manage(AppState {
db: DbConnection(Arc::new(Mutex::new(None))),
hlc: Mutex::new(HlcService::new()), // Starte mit einem uninitialisierten HLC
hlc: Mutex::new(HlcService::new()),
extension_manager: ExtensionManager::new(),
})
.manage(ExtensionState::default())
.plugin(tauri_plugin_dialog::init())
@ -75,7 +69,6 @@ pub fn run() {
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_persisted_scope::init())
.plugin(tauri_plugin_store::Builder::new().build())
//.plugin(tauri_plugin_android_fs::init())
.invoke_handler(tauri::generate_handler![
database::create_encrypted_database,
database::delete_vault,
@ -86,36 +79,13 @@ pub fn run() {
database::vault_exists,
extension::database::extension_sql_execute,
extension::database::extension_sql_select,
//database::update_hlc_from_remote,
/* extension::copy_directory,
extension::database::extension_sql_select, */
extension::get_all_extensions,
extension::get_extension_info,
extension::install_extension_with_permissions,
extension::is_extension_installed,
extension::preview_extension,
extension::remove_extension,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
/* fn extension_protocol_handler(
app_handle: &tauri::AppHandle, // Beachten Sie die Signaturänderung in neueren Tauri-Versionen
request: &tauri::http::Request<Vec<u8>>,
) -> Result<tauri::http::Response<Vec<u8>>, Box<dyn std::error::Error + Send + Sync>> {
let uri_str = request.uri().to_string();
let parsed_url = match Url::parse(&uri_str) {
Ok(url) => url,
Err(e) => {
eprintln!("Fehler beim Parsen der URL '{}': {}", uri_str, e);
return Ok(tauri::http::ResponseBuilder::new().status(400).body(Vec::from("Ungültige URL"))?);
}
};
let plugin_id = parsed_url.host_str().ok_or_else(|| "Fehlende Plugin-ID in der URL".to_string())?;
let path_segments: Vec<&str> = parsed_url.path_segments().ok_or_else(|| "URL hat keinen Pfad".to_string())?.collect();
if path_segments.len() < 2 {
eprintln!("Unvollständiger Pfad in URL: {}", uri_str);
return Ok(tauri::http::Response::new().status(400).body(Vec::from("Unvollständiger Pfad"))?);
}
let version = path_segments;
let file_path = path_segments[1..].join("/");
Ok(tauri::http::Response::builder()::new().status(404).body(Vec::new())?)
} */

View File

@ -4,20 +4,9 @@
v-bind="$attrs"
>
<div class="absolute top-2 right-2">
<UiDropdown class="btn btn-sm btn-text btn-circle">
<template #activator>
<Icon name="mdi:dots-vertical" />
</template>
<template #items>
<UiButton
class="btn-error btn-outline btn-sm"
@click="showRemoveDialog = true"
>
<Icon name="mdi:trash" /> {{ t('remove') }}
</UiButton>
</template>
</UiDropdown>
<UDropdownMenu>
<UiButton icon="mdi:dots-vertical" />
</UDropdownMenu>
</div>
<div class="card-header">
@ -44,6 +33,7 @@
<div class="card-actions" v-if="$slots.action">
<slot name="action" />
</div> -->
hier klicken
<div
class="size-20 absolute bottom-2 right-2"
v-html="icon"

View File

@ -1,71 +1,107 @@
<template>
<UiAccordion v-if="database?.read?.length">
<UAccordion v-if="database?.read?.length">
<template #title>
<h3>{{ t("permission.read") }}</h3>
<h3>{{ t('permission.read') }}</h3>
</template>
<ul class="space-y-0.5">
<li v-for="read in database?.read" class="flex items-center justify-between px-4 py-1">
<div class="flex items-center gap-1">
<input :id="Object.keys(read).at(0)" type="checkbox" class="checkbox" :checked="Object.values(read).at(0)" >
<label class="label-text text-base" :for="Object.keys(read).at(0)">{{ Object.keys(read).at(0) }}</label>
</div>
</li>
</ul>
</UiAccordion>
<UiAccordion v-if="database?.write?.length">
<template #title>
<h3>{{ t("permission.write") }}</h3>
</template>
<ul class="space-y-0.5">
<li v-for="write in database?.write" class="flex items-center justify-between px-4 py-0.5">
<li
v-for="read in database?.read"
class="flex items-center justify-between px-4 py-1"
>
<div class="flex items-center gap-1">
<input
:id="Object.keys(write).at(0)" type="checkbox" class="checkbox"
:checked="Object.values(write).at(0)" >
<label class="label-text text-base" :for="Object.keys(write).at(0)">{{ Object.keys(write).at(0) }}</label>
:id="Object.keys(read).at(0)"
type="checkbox"
class="checkbox"
:checked="Object.values(read).at(0)"
/>
<label
class="label-text text-base"
:for="Object.keys(read).at(0)"
>{{ Object.keys(read).at(0) }}</label
>
</div>
</li>
</ul>
</UiAccordion>
</UAccordion>
<UiAccordion v-if="database?.create?.length">
<UAccordion v-if="database?.write?.length">
<template #title>
<h3>{{ t("permission.create") }}</h3>
<h3>{{ t('permission.write') }}</h3>
</template>
<ul class="space-y-0.5">
<li v-for="create in database?.create" class="flex items-center justify-between px-4 py-0.5">
<li
v-for="write in database?.write"
class="flex items-center justify-between px-4 py-0.5"
>
<div class="flex items-center gap-1">
<input
:id="Object.keys(create).at(0)" type="checkbox" class="checkbox"
:checked="Object.values(create).at(0)" >
<label class="label-text text-base" :for="Object.keys(create).at(0)">{{ Object.keys(create).at(0) }}</label>
:id="Object.keys(write).at(0)"
type="checkbox"
class="checkbox"
:checked="Object.values(write).at(0)"
/>
<label
class="label-text text-base"
:for="Object.keys(write).at(0)"
>{{ Object.keys(write).at(0) }}</label
>
</div>
</li>
</ul>
</UiAccordion>
</UAccordion>
<UAccordion v-if="database?.create?.length">
<template #title>
<h3>{{ t('permission.create') }}</h3>
</template>
<ul class="space-y-0.5">
<li
v-for="create in database?.create"
class="flex items-center justify-between px-4 py-0.5"
>
<div class="flex items-center gap-1">
<input
:id="Object.keys(create).at(0)"
type="checkbox"
class="checkbox"
:checked="Object.values(create).at(0)"
/>
<label
class="label-text text-base"
:for="Object.keys(create).at(0)"
>{{ Object.keys(create).at(0) }}</label
>
</div>
</li>
</ul>
</UAccordion>
</template>
<script setup lang="ts">
defineProps<{ database?: { read?: Record<string, boolean>[], write?: Record<string, boolean>[], create?: Record<string, boolean>[] } }>();
const { t } = useI18n();
defineProps<{
database?: {
read?: Record<string, boolean>[]
write?: Record<string, boolean>[]
create?: Record<string, boolean>[]
}
}>()
const { t } = useI18n()
</script>
<i18n lang="yaml">
de:
permission:
read: Lesen
write: Schreiben
create: Erstellen
de:
permission:
read: Lesen
write: Schreiben
create: Erstellen
en:
permission:
read: Read
write: Write
create: Create
en:
permission:
read: Read
write: Write
create: Create
</i18n>

View File

@ -1,38 +1,56 @@
<template>
<UiAccordion v-if="filesystem?.read?.length">
<UAccordion v-if="filesystem?.read?.length">
<template #title>
<h3>{{ t('permission.read') }}</h3>
</template>
<ul class="space-y-0.5">
<li v-for="read in filesystem?.read" class="flex items-center justify-between px-4 py-0.5">
<li
v-for="read in filesystem?.read"
class="flex items-center justify-between px-4 py-0.5"
>
<div class="flex items-center gap-1">
<input :id="Object.keys(read).at(0)" type="checkbox" class="checkbox" :checked="Object.values(read).at(0)" >
<label class="label-text text-base" :for="Object.keys(read).at(0)">{{
Object.keys(read).at(0)
}}</label>
<input
:id="Object.keys(read).at(0)"
type="checkbox"
class="checkbox"
:checked="Object.values(read).at(0)"
/>
<label
class="label-text text-base"
:for="Object.keys(read).at(0)"
>{{ Object.keys(read).at(0) }}</label
>
</div>
</li>
</ul>
</UiAccordion>
</UAccordion>
<UiAccordion v-if="filesystem?.write?.length">
<UAccordion v-if="filesystem?.write?.length">
<template #title>
<h3>{{ t('permission.write') }}</h3>
</template>
<ul class="space-y-0.5">
<li v-for="write in filesystem?.write" class="flex items-center justify-between px-4 py-0.5">
<li
v-for="write in filesystem?.write"
class="flex items-center justify-between px-4 py-0.5"
>
<div class="flex items-center gap-1">
<input
:id="Object.keys(write).at(0)" type="checkbox" class="checkbox"
:checked="Object.values(write).at(0)" >
<label class="label-text text-base" :for="Object.keys(write).at(0)">{{
Object.keys(write).at(0)
}}</label>
:id="Object.keys(write).at(0)"
type="checkbox"
class="checkbox"
:checked="Object.values(write).at(0)"
/>
<label
class="label-text text-base"
:for="Object.keys(write).at(0)"
>{{ Object.keys(write).at(0) }}</label
>
</div>
</li>
</ul>
</UiAccordion>
</UAccordion>
</template>
<script setup lang="ts">
@ -46,13 +64,13 @@ const { t } = useI18n()
</script>
<i18n lang="yaml">
de:
permission:
read: Lesen
write: Schreiben
de:
permission:
read: Lesen
write: Schreiben
en:
permission:
read: Read
write: Write
en:
permission:
read: Read
write: Write
</i18n>

View File

@ -1,33 +1,43 @@
<template>
<UiAccordion>
<UAccordion>
<template #title>
<h3>{{ t("http.access") }}</h3>
<h3>{{ t('http.access') }}</h3>
</template>
<ul class="space-y-0.5">
<li v-for="access in http" class="flex items-center justify-between px-4 py-0.5">
<li
v-for="access in http"
class="flex items-center justify-between px-4 py-0.5"
>
<div class="flex items-center gap-1">
<input
:id="Object.keys(access).at(0)" type="checkbox" class="checkbox"
:checked="Object.values(access).at(0)" >
<label class="label-text text-base" :for="Object.keys(access).at(0)">{{ Object.keys(access).at(0) }}</label>
:id="Object.keys(access).at(0)"
type="checkbox"
class="checkbox"
:checked="Object.values(access).at(0)"
/>
<label
class="label-text text-base"
:for="Object.keys(access).at(0)"
>{{ Object.keys(access).at(0) }}</label
>
</div>
</li>
</ul>
</UiAccordion>
</UAccordion>
</template>
<script setup lang="ts">
defineProps<{ http?: Record<string, boolean>[] }>();
const { t } = useI18n();
defineProps<{ http?: Record<string, boolean>[] }>()
const { t } = useI18n()
</script>
<i18n lang="yaml">
de:
http:
access: Internet Zugriff
de:
http:
access: Internet Zugriff
en:
http:
access: Internet Access
en:
http:
access: Internet Access
</i18n>

View File

@ -1,15 +1,17 @@
/**
* Broadcasts context changes to all active extensions
*/
// composables/extensionContextBroadcast.ts
export const useExtensionContextBroadcast = () => {
const extensionIframes = ref<HTMLIFrameElement[]>([])
// Globaler State für alle aktiven IFrames
const extensionIframes = useState<Set<HTMLIFrameElement>>(
'extension-iframes',
() => new Set(),
)
const registerExtensionIframe = (iframe: HTMLIFrameElement) => {
extensionIframes.value.push(iframe)
extensionIframes.value.add(iframe)
}
const unregisterExtensionIframe = (iframe: HTMLIFrameElement) => {
extensionIframes.value = extensionIframes.value.filter((f) => f !== iframe)
extensionIframes.value.delete(iframe)
}
const broadcastContextChange = (context: {

View File

@ -1,4 +1,6 @@
import type { IHaexHubExtensionLink } from '~/types/haexhub'
// composables/extensionMessageHandler.ts
import { invoke } from '@tauri-apps/api/core'
import type { IHaexHubExtension } from '~/types/haexhub'
interface ExtensionRequest {
id: string
@ -7,119 +9,124 @@ interface ExtensionRequest {
timestamp: number
}
interface ExtensionResponse {
id: string
result?: unknown
error?: {
code: string
message: string
details?: unknown
}
}
// Globaler Handler - nur einmal registriert
let globalHandlerRegistered = false
const iframeRegistry = new Map<HTMLIFrameElement, IHaexHubExtension>()
export const useExtensionMessageHandler = (
iframeRef: Ref<HTMLIFrameElement | undefined | null>,
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
) => {
const handleMessage = async (event: MessageEvent) => {
// Security: Only accept messages from our iframe
if (!iframeRef.value || event.source !== iframeRef.value.contentWindow) {
return
const registerGlobalMessageHandler = () => {
if (globalHandlerRegistered) return
window.addEventListener('message', async (event: MessageEvent) => {
// Finde die Extension für dieses IFrame
let extension: IHaexHubExtension | undefined
let sourceIframe: HTMLIFrameElement | undefined
for (const [iframe, ext] of iframeRegistry.entries()) {
if (event.source === iframe.contentWindow) {
extension = ext
sourceIframe = iframe
break
}
}
if (!extension || !sourceIframe) {
return // Message ist nicht von einem registrierten IFrame
}
const request = event.data as ExtensionRequest
// Validate request structure
if (!request.id || !request.method) {
console.error('Invalid extension request:', request)
return
}
console.log('[HaexHub] Extension request:', request.method, request.params)
console.log(
`[HaexHub] ${extension.name} request:`,
request.method,
request.params,
)
try {
let result: unknown
// Route request to appropriate handler
if (request.method.startsWith('extension.')) {
result = await handleExtensionMethod(request, extension)
result = await handleExtensionMethodAsync(request, extension)
} else if (request.method.startsWith('db.')) {
result = await handleDatabaseMethod(request, extension)
result = await handleDatabaseMethodAsync(request, extension)
} else if (request.method.startsWith('fs.')) {
result = await handleFilesystemMethodAsync(request, extension)
} else if (request.method.startsWith('http.')) {
result = await handleHttpMethodAsync(request, extension)
} else if (request.method.startsWith('permissions.')) {
result = await handlePermissionsMethod(request, extension)
result = await handlePermissionsMethodAsync(request, extension)
} else if (request.method.startsWith('context.')) {
result = await handleContextMethod(request)
} else if (request.method.startsWith('search.')) {
result = await handleSearchMethod(request, extension)
result = await handleContextMethodAsync(request)
} else {
throw new Error(`Unknown method: ${request.method}`)
}
// Send success response
sendResponse(iframeRef.value, {
id: request.id,
result,
})
sourceIframe.contentWindow?.postMessage(
{
id: request.id,
result,
},
'*',
)
} catch (error) {
console.error('[HaexHub] Extension request error:', error)
// Send error response
sendResponse(iframeRef.value, {
id: request.id,
error: {
code: 'INTERNAL_ERROR',
message: error instanceof Error ? error.message : 'Unknown error',
details: error,
sourceIframe.contentWindow?.postMessage(
{
id: request.id,
error: {
code: 'INTERNAL_ERROR',
message: error instanceof Error ? error.message : 'Unknown error',
details: error,
},
},
})
'*',
)
}
}
const sendResponse = (
iframe: HTMLIFrameElement,
response: ExtensionResponse,
) => {
iframe.contentWindow?.postMessage(response, '*')
}
// Register/unregister message listener
onMounted(() => {
window.addEventListener('message', handleMessage)
})
globalHandlerRegistered = true
}
export const useExtensionMessageHandler = (
iframeRef: Ref<HTMLIFrameElement | undefined | null>,
extension: ComputedRef<IHaexHubExtension | undefined | null>,
) => {
// Registriere globalen Handler beim ersten Aufruf
registerGlobalMessageHandler()
// Registriere dieses IFrame
watchEffect(() => {
if (iframeRef.value && extension.value) {
iframeRegistry.set(iframeRef.value, extension.value)
}
})
// Cleanup beim Unmount
onUnmounted(() => {
window.removeEventListener('message', handleMessage)
if (iframeRef.value) {
iframeRegistry.delete(iframeRef.value)
}
})
return {
handleMessage,
}
}
// ==========================================
// Extension Methods
// ==========================================
async function handleExtensionMethod(
async function handleExtensionMethodAsync(
request: ExtensionRequest,
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
extension: IHaexHubExtension, // Direkter Typ, kein ComputedRef mehr
) {
switch (request.method) {
case 'extension.getInfo':
return {
keyHash: extension.value?.id || '', // TODO: Real key hash
name: extension.value?.name || '',
fullId: `${extension.value?.id}/${extension.value?.name}@${extension.value?.version}`,
version: extension.value?.version || '',
displayName: extension.value?.name,
namespace: extension.value?.author,
allowedOrigin: window.location.origin, // "tauri://localhost"
}
case 'extensions.getDependencies':
// TODO: Implement dependencies from manifest
return []
return await invoke('get_extension_info', {
extensionId: extension.id,
})
default:
throw new Error(`Unknown extension method: ${request.method}`)
}
@ -129,47 +136,41 @@ async function handleExtensionMethod(
// Database Methods
// ==========================================
async function handleDatabaseMethod(
async function handleDatabaseMethodAsync(
request: ExtensionRequest,
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
extension: IHaexHubExtension, // Direkter Typ
) {
const { currentVault } = useVaultStore()
if (!currentVault) {
throw new Error('No vault available')
const params = request.params as {
query?: string
params?: unknown[]
}
if (!extension.value) {
throw new Error('Extension not found')
}
const params = request.params as { query?: string; params?: unknown[] }
switch (request.method) {
case 'db.query': {
// Validate permission
await validateDatabaseAccess(extension.value, params.query || '', 'read')
// Execute query
const result = await currentVault.drizzle.execute(params.query || '')
const rows = await invoke<unknown[]>('extension_sql_select', {
sql: params.query || '',
params: params.params || [],
extensionId: extension.id,
})
return {
rows: result.rows || [],
rows,
rowsAffected: 0,
lastInsertId: undefined,
}
}
case 'db.execute': {
// Validate permission
await validateDatabaseAccess(extension.value, params.query || '', 'write')
// Execute query
const result = await currentVault.drizzle.execute(params.query || '')
await invoke<string[]>('extension_sql_execute', {
sql: params.query || '',
params: params.params || [],
extensionId: extension.id,
})
return {
rows: [],
rowsAffected: result.rowsAffected || 0,
lastInsertId: result.lastInsertId,
rowsAffected: 1,
lastInsertId: undefined,
}
}
@ -177,18 +178,14 @@ async function handleDatabaseMethod(
const statements =
(request.params as { statements?: string[] }).statements || []
// Validate all statements
for (const stmt of statements) {
await validateDatabaseAccess(extension.value, stmt, 'write')
await invoke('extension_sql_execute', {
sql: stmt,
params: [],
extensionId: extension.id,
})
}
// Execute transaction
await currentVault.drizzle.transaction(async (tx) => {
for (const stmt of statements) {
await tx.execute(stmt)
}
})
return { success: true }
}
@ -196,125 +193,63 @@ async function handleDatabaseMethod(
throw new Error(`Unknown database method: ${request.method}`)
}
}
// ==========================================
// Permission Validation
// Filesystem Methods (TODO)
// ==========================================
async function validateDatabaseAccess(
extension: IHaexHubExtensionLink,
query: string,
operation: 'read' | 'write',
): Promise<void> {
// Extract table name from query
const tableMatch = query.match(/(?:FROM|INTO|UPDATE|TABLE)\s+(\w+)/i)
if (!tableMatch) {
throw new Error('Could not extract table name from query')
}
const tableName = tableMatch[1]
// Check if it's the extension's own table
const extensionPrefix = `${extension.id}_${extension.name?.replace(/-/g, '_')}_`
const isOwnTable = tableName.startsWith(extensionPrefix)
if (isOwnTable) {
// Own tables: always allowed
return
}
// External table: Check permissions
const hasPermission = await checkDatabasePermission(
extension.id,
tableName,
operation,
)
if (!hasPermission) {
throw new Error(`Permission denied: ${operation} access to ${tableName}`)
}
}
async function checkDatabasePermission(
extensionId: string,
tableName: string,
operation: 'read' | 'write',
): Promise<boolean> {
// TODO: Query permissions from database
// SELECT * FROM db_extension_permissions
// WHERE extension_id = ? AND resource = ? AND operation = ?
console.warn('TODO: Implement permission check', {
extensionId,
tableName,
operation,
})
// For now: deny by default
return false
}
// ==========================================
// Permission Methods
// ==========================================
async function handlePermissionsMethod(
async function handleFilesystemMethodAsync(
request: ExtensionRequest,
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
extension: IHaexHubExtension,
) {
switch (request.method) {
case 'permissions.database.request': {
const params = request.params as {
resource: string
operation: 'read' | 'write'
reason?: string
}
if (!request || !extension) return
// TODO: Implementiere Filesystem Commands im Backend
throw new Error('Filesystem methods not yet implemented')
}
// TODO: Show user dialog to grant/deny permission
console.log('[HaexHub] Permission request:', params)
// ==========================================
// HTTP Methods (TODO)
// ==========================================
// For now: return ASK
return {
status: 'ask',
permanent: false,
}
}
case 'permissions.database.check': {
const params = request.params as {
resource: string
operation: 'read' | 'write'
}
const hasPermission = await checkDatabasePermission(
extension.value?.id || '',
params.resource,
params.operation,
)
return {
status: hasPermission ? 'granted' : 'denied',
permanent: true,
}
}
default:
throw new Error(`Unknown permission method: ${request.method}`)
async function handleHttpMethodAsync(
request: ExtensionRequest,
extension: IHaexHubExtension,
) {
if (!extension || !request) {
throw new Error('Extension not found')
}
// TODO: Implementiere HTTP Commands im Backend
throw new Error('HTTP methods not yet implemented')
}
// ==========================================
// Permission Methods (TODO)
// ==========================================
async function handlePermissionsMethodAsync(
request: ExtensionRequest,
extension: IHaexHubExtension,
) {
if (!extension || !request) {
throw new Error('Extension not found')
}
// TODO: Implementiere Permission Request UI
throw new Error('Permission methods not yet implemented')
}
// ==========================================
// Context Methods
// ==========================================
async function handleContextMethod(request: ExtensionRequest) {
const { theme } = useThemeStore()
async function handleContextMethodAsync(request: ExtensionRequest) {
const { currentTheme } = storeToRefs(useUiStore())
const { locale } = useI18n()
switch (request.method) {
case 'context.get':
return {
theme: theme.value || 'system',
theme: currentTheme.value || 'system',
locale: locale.value,
platform: detectPlatform(),
}
@ -330,29 +265,3 @@ function detectPlatform(): 'desktop' | 'mobile' | 'tablet' {
if (width < 1024) return 'tablet'
return 'desktop'
}
// ==========================================
// Search Methods
// ==========================================
async function handleSearchMethod(
request: ExtensionRequest,
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
) {
switch (request.method) {
case 'search.respond': {
const params = request.params as {
requestId: string
results: unknown[]
}
// TODO: Store search results for display
console.log('[HaexHub] Search results from extension:', params)
return { success: true }
}
default:
throw new Error(`Unknown search method: ${request.method}`)
}
}

View File

@ -1,7 +1,7 @@
<template>
<UPage
<div
:ui="{
root: ['h-full w-full bg-elevated'],
root: ['h-full w-full bg-elevated lg:flex'],
center: ['h-full w-full'],
}"
>
@ -34,7 +34,7 @@
</template>
</UiDialogConfirm>
</div>
</UPage>
</div>
</template>
<script setup lang="ts">

View File

@ -1,24 +1,65 @@
<template>
<div class="w-full h-full overflow-scroll">
<div
v-if="!iFrameSrc"
class="flex items-center justify-center h-full"
>
<p>{{ t('loading') }}</p>
<div class="h-full flex flex-col">
<!-- Tab Bar -->
<div class="flex gap-2 p-2 bg-default overflow-x-auto border-b">
<div
v-for="tab in tabsStore.sortedTabs"
:key="tab.extension.id"
:class="[
'btn btn-sm gap-2',
tabsStore.activeTabId === tab.extension.id
? 'btn-primary'
: 'btn-ghost',
]"
@click="tabsStore.setActiveTab(tab.extension.id)"
>
{{ tab.extension.name }}
<button
class="ml-1 hover:text-error"
@click.stop="tabsStore.closeTab(tab.extension.id)"
>
<Icon
name="mdi:close"
size="16"
/>
</button>
</div>
</div>
<!-- IFrame Container -->
<div class="flex-1 relative overflow-hidden">
<div
v-for="tab in tabsStore.sortedTabs"
:key="tab.extension.id"
:style="{ display: tab.isVisible ? 'block' : 'none' }"
class="w-full h-full"
>
<iframe
:ref="
(el) => registerIFrame(tab.extension.id, el as HTMLIFrameElement)
"
class="w-full h-full"
:src="getExtensionUrl(tab.extension)"
sandbox="allow-scripts"
allow="autoplay; speaker-selection; encrypted-media;"
/>
</div>
<!-- Loading State -->
<div
v-if="tabsStore.tabCount === 0"
class="flex items-center justify-center h-full"
>
<p>{{ t('loading') }}</p>
</div>
</div>
<iframe
v-else
ref="iFrameRef"
class="w-full h-full"
:src="iFrameSrc"
sandbox="allow-scripts "
allow="autoplay; speaker-selection; encrypted-media;"
/>
</div>
</template>
<script setup lang="ts">
import { useExtensionMessageHandler } from '~/composables/extensionMessageHandler'
import { useExtensionTabsStore } from '~/stores/extensions/tabs'
import type { IHaexHubExtension } from '~/types/haexhub'
definePageMeta({
name: 'haexExtension',
@ -26,42 +67,84 @@ definePageMeta({
const { t } = useI18n()
const iFrameRef = useTemplateRef('iFrameRef')
const tabsStore = useExtensionTabsStore()
const { extensionEntry: iframeSrc, currentExtension } =
storeToRefs(useExtensionsStore())
// Extension aus Route öffnen
//const extensionId = computed(() => route.params.extensionId as string)
const iFrameSrc = computed(() =>
iframeSrc.value ? `${iframeSrc.value}/index.html` : '',
const { currentExtensionId } = storeToRefs(useExtensionsStore())
watchEffect(() => {
if (currentExtensionId.value) {
tabsStore.openTab(currentExtensionId.value)
}
})
const messageHandlers = new Map<string, boolean>()
watch(
() => tabsStore.openTabs,
(tabs) => {
tabs.forEach((tab, id) => {
if (tab.iframe && !messageHandlers.has(id)) {
const iframeRef = ref(tab.iframe)
const extensionRef = computed(() => tab.extension)
useExtensionMessageHandler(iframeRef, extensionRef)
messageHandlers.set(id, true)
}
})
},
{ deep: true },
)
useExtensionMessageHandler(iFrameRef, currentExtension)
// IFrame Registrierung und Message Handler Setup
/* const iframeRefs = new Map<string, HTMLIFrameElement>()
const setupMessageHandlers = new Set<string>() */
const registerIFrame = (extensionId: string, el: HTMLIFrameElement | null) => {
if (!el) return
tabsStore.registerIFrame(extensionId, el)
}
// Extension URL generieren
const getExtensionUrl = (extension: IHaexHubExtension) => {
const info = { id: extension.id, version: extension.version }
const jsonString = JSON.stringify(info)
const bytes = new TextEncoder().encode(jsonString)
const encoded = Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
const url = `haex-extension://${encoded}/index.html`
console.log('Extension URL:', url, 'for', extension.name)
return url
}
// Context Changes an alle Tabs broadcasten
const { currentTheme } = storeToRefs(useUiStore())
const { locale } = useI18n()
watch([currentTheme, locale], () => {
if (iFrameRef.value?.contentWindow) {
iFrameRef.value.contentWindow.postMessage(
{
type: 'context.changed',
data: {
context: {
theme: currentTheme.value || 'system',
locale: locale.value,
platform:
window.innerWidth < 768
? 'mobile'
: window.innerWidth < 1024
? 'tablet'
: 'desktop',
},
},
timestamp: Date.now(),
tabsStore.broadcastToAllTabs({
type: 'context.changed',
data: {
context: {
theme: currentTheme.value || 'system',
locale: locale.value,
platform:
window.innerWidth < 768
? 'mobile'
: window.innerWidth < 1024
? 'tablet'
: 'desktop',
},
'*',
)
}
},
timestamp: Date.now(),
})
})
// Cleanup beim Verlassen
onBeforeUnmount(() => {
// Optional: Alle Tabs schließen oder offen lassen
// tabsStore.closeAllTabs()
})
</script>

View File

@ -31,18 +31,17 @@
class="size-full md:size-2/3 md:translate-x-1/5 md:translate-y-1/3"
/>
<div class="fixed top-30 right-10">
<UiTooltip :tooltip="t('extension.add')">
<UiButton
class="btn-square btn-primary btn-xl btn-gradient rotate-45"
@click="prepareInstallExtensionAsync"
>
<Icon
name="mdi:plus"
size="1.5em"
class="rotate-45"
/>
</UiButton>
</UiTooltip>
<UiButton
class="btn-square btn-primary btn-xl btn-gradient rotate-45"
:tooltip="t('extension.add')"
@click="prepareInstallExtensionAsync"
>
<Icon
name="mdi:plus"
size="1.5em"
class="rotate-45"
/>
</UiButton>
</div>
</div>
@ -93,7 +92,7 @@ const extension = reactive<{
path: '',
})
const loadExtensionManifestAsync = async () => {
/* const loadExtensionManifestAsync = async () => {
try {
extension.path = await open({ directory: true, recursive: true })
if (!extension.path) return
@ -111,7 +110,7 @@ const loadExtensionManifestAsync = async () => {
add({ color: 'error', description: JSON.stringify(error) })
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
}
}
} */
const { add } = useToast()
const { addNotificationAsync } = useNotificationStore()

View File

@ -1,12 +1,12 @@
<template>
<UPage>
<div class="w-full">
<div class="h-screen bg-amber-300 flex-1 flex-wrap">
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
</div>
<div class="h-screen bg-teal-300 flex-1">
abbbbbbbbbbbbbbbbbbbbb availableThemes:{{ uiStore.availableThemes }}
</div>
</UPage>
</div>
</template>
<script setup lang="ts">

View File

@ -1,22 +1,44 @@
import { invoke } from '@tauri-apps/api/core'
import { appDataDir, join } from '@tauri-apps/api/path'
import { exists, readDir, readTextFile, remove } from '@tauri-apps/plugin-fs'
import { and, eq } from 'drizzle-orm'
import type {
IHaexHubExtension,
IHaexHubExtensionLink,
IHaexHubExtensionManifest,
} from '~/types/haexhub'
import { haexExtensions } from '~~/src-tauri/database/schemas/vault'
const manifestFileName = 'manifest.json'
const logoFileName = 'icon.svg'
interface ExtensionInfoResponse {
key_hash: string
name: string
full_id: string
version: string
display_name: string | null
namespace: string | null
allowed_origin: string
}
/* const manifestFileName = 'manifest.json'
const logoFileName = 'icon.svg' */
export const useExtensionsStore = defineStore('extensionsStore', () => {
const availableExtensions = ref<IHaexHubExtensionLink[]>([])
const { addNotificationAsync } = useNotificationStore()
const availableExtensions = ref<IHaexHubExtension[]>([])
const currentRoute = useRouter().currentRoute
const extensionLinks = computed<ISidebarItem[]>(() =>
const currentExtensionId = computed(() =>
getSingleRouteParam(currentRoute.value.params.extensionId),
)
const currentExtension = computed(() => {
if (!currentExtensionId.value) return null
return (
availableExtensions.value.find(
(ext) => ext.id === currentExtensionId.value,
) ?? null
)
})
/* const { addNotificationAsync } = useNotificationStore() */
/* const extensionLinks = computed<ISidebarItem[]>(() =>
availableExtensions.value
.filter((extension) => extension.enabled && extension.installed)
.map((extension) => ({
@ -26,9 +48,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
tooltip: extension.name ?? '',
to: { name: 'haexExtension', params: { extensionId: extension.id } },
})),
)
const currentRoute = useRouter().currentRoute
) */
const isActive = (id: string) =>
computed(
@ -37,32 +57,26 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
currentRoute.value.params.extensionId === id,
)
const currentExtension = computed(() => {
console.log('computed currentExtension', currentRoute.value.params)
if (currentRoute.value.meta.name !== 'haexExtension') return
const extensionEntry = computed(() => {
if (!currentExtension.value?.version || !currentExtension.value?.id)
return null
const extensionId = getSingleRouteParam(
currentRoute.value.params.extensionId,
const encodedInfo = encodeExtensionInfo(
currentExtension.value.id,
currentExtension.value.version,
)
console.log('extensionId from param', extensionId)
if (!extensionId) return
const extension = availableExtensions.value.find(
(extension) => extension.id === extensionId,
)
console.log('currentExtension', extension)
return extension
return `extension://${encodedInfo}`
})
const getExtensionPathAsync = async (
/* const getExtensionPathAsync = async (
extensionId?: string,
version?: string,
) => {
if (!extensionId || !version) return ''
return await join(await appDataDir(), 'extensions', extensionId, version)
}
} */
const checkSourceExtensionDirectoryAsync = async (
/* const checkSourceExtensionDirectoryAsync = async (
extensionDirectory: string,
) => {
try {
@ -82,22 +96,154 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
//throw error //new Error(`Keine Leseberechtigung für Ordner ${extensionDirectory}`);
}
} */
const loadExtensionsAsync = async () => {
try {
const extensions =
await invoke<ExtensionInfoResponse[]>('get_all_extensions')
availableExtensions.value = extensions.map((ext) => ({
id: ext.key_hash,
name: ext.display_name || ext.name,
version: ext.version,
author: ext.namespace,
icon: null,
enabled: true,
}))
} catch (error) {
console.error('Fehler beim Laden der Extensions:', error)
throw error
}
}
const isExtensionInstalledAsync = async (
extension: Partial<IHaexHubExtension>,
) => {
/* const loadExtensionsAsync = async () => {
const { currentVault } = storeToRefs(useVaultStore())
const extensions =
(await currentVault.value?.drizzle.select().from(haexExtensions)) ?? []
//if (!extensions?.length) return false;
const installedExtensions = await filterAsync(
extensions,
isExtensionInstalledAsync,
)
console.log('loadExtensionsAsync installedExtensions', installedExtensions)
availableExtensions.value =
extensions.map((extension) => ({
id: extension.id,
name: extension.name ?? '',
icon: extension.icon ?? '',
author: extension.author ?? '',
version: extension.version ?? '',
enabled: extension.enabled ? true : false,
installed: installedExtensions.includes(extension),
})) ?? []
console.log('loadExtensionsAsync', availableExtensions.value)
return true
} */
const installAsync = async (sourcePath: string | null) => {
if (!sourcePath) throw new Error('Kein Pfad angegeben')
try {
const extensionPath = await getExtensionPathAsync(
extension.id,
`${extension.version}`,
)
console.log(
`extension ${extension.id} is installed ${await exists(extensionPath)}`,
)
return await exists(extensionPath)
const extensionId = await invoke<string>('install_extension', {
sourcePath,
})
return extensionId
} catch (error) {
console.error(error)
console.error('Fehler bei Extension-Installation:', error)
throw error
}
}
/* const installAsync = async (extensionDirectory: string | null) => {
try {
if (!extensionDirectory)
throw new Error('Kein Ordner für Erweiterung angegeben')
const manifestPath = await join(extensionDirectory, manifestFileName)
const manifest = (await JSON.parse(
await readTextFile(manifestPath),
)) as IHaexHubExtensionManifest
const destination = await getExtensionPathAsync(
manifest.id,
manifest.version,
)
await checkSourceExtensionDirectoryAsync(extensionDirectory)
await invoke('copy_directory', {
source: extensionDirectory,
destination,
})
const logoFilePath = await join(destination, logoFileName)
const logo = await readTextFile(logoFilePath)
const { currentVault } = storeToRefs(useVaultStore())
const res = await currentVault.value?.drizzle
.insert(haexExtensions)
.values({
id: manifest.id,
name: manifest.name,
author: manifest.author,
enabled: true,
url: manifest.url,
version: manifest.version,
icon: logo,
})
console.log('insert extensions', res)
addNotificationAsync({
type: 'success',
text: `${manifest.name} wurde installiert`,
})
} catch (error) {
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
throw error
}
} */
const removeExtensionAsync = async (extensionId: string, version: string) => {
try {
await invoke('remove_extension', {
extensionId,
extensionVersion: version,
})
} catch (error) {
console.error('Fehler beim Entfernen der Extension:', error)
throw error
}
}
/* const removeExtensionAsync = async (id: string, version: string) => {
try {
console.log('remove extension', id, version)
await removeExtensionFromVaultAsync(id, version)
await removeExtensionFilesAsync(id, version)
} catch (error) {
throw new Error(JSON.stringify(error))
}
} */
const isExtensionInstalledAsync = async ({
id,
version,
}: {
id: string
version: string
}) => {
try {
return await invoke<boolean>('is_extension_installed', {
extensionId: id,
extensionVersion: version,
})
} catch (error) {
console.error('Fehler beim Prüfen der Extension:', error)
return false
}
}
@ -156,7 +302,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
return true
}
const readManifestFileAsync = async (
/* const readManifestFileAsync = async (
extensionId: string,
version: string,
) => {
@ -173,173 +319,17 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
await readTextFile(manifestPath),
)) as IHaexHubExtensionManifest
/*
TODO implement check, that manifest has valid data
*/
return manifest
} catch (error) {
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
console.error('ERROR readManifestFileAsync', error)
}
}
} */
const installAsync = async (extensionDirectory: string | null) => {
try {
if (!extensionDirectory)
throw new Error('Kein Ordner für Erweiterung angegeben')
const manifestPath = await join(extensionDirectory, manifestFileName)
const manifest = (await JSON.parse(
await readTextFile(manifestPath),
)) as IHaexHubExtensionManifest
const destination = await getExtensionPathAsync(
manifest.id,
manifest.version,
)
await checkSourceExtensionDirectoryAsync(extensionDirectory)
await invoke('copy_directory', {
source: extensionDirectory,
destination,
})
const logoFilePath = await join(destination, logoFileName)
const logo = await readTextFile(logoFilePath)
const { currentVault } = storeToRefs(useVaultStore())
const res = await currentVault.value?.drizzle
.insert(haexExtensions)
.values({
id: manifest.id,
name: manifest.name,
author: manifest.author,
enabled: true,
url: manifest.url,
version: manifest.version,
icon: logo,
})
console.log('insert extensions', res)
addNotificationAsync({
type: 'success',
text: `${manifest.name} wurde installiert`,
})
} catch (error) {
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
throw error
/*
const resourcePath = await resourceDir();
//const manifestPath = await join(extensionDirectory, 'manifest.json');
const manifestPath = await join(
resourcePath,
'extension',
'demo-addon',
'manifest.json'
);
const regex = /((href|src)=["'])([^"']+)(["'])/g;
let htmlContent = await readTextFile(
await join(resourcePath, 'extension', 'demo-addon', 'index.html')
);
const replacements = [];
let match;
while ((match = regex.exec(htmlContent)) !== null) {
const [fullMatch, prefix, attr, resource, suffix] = match;
if (!resource.startsWith('http')) {
replacements.push({ match: fullMatch, resource, prefix, suffix });
}
}
for (const { match, resource, prefix, suffix } of replacements) {
const fileContent = await readTextFile(
await join(resourcePath, 'extension', 'demo-addon', resource)
);
const blob = new Blob([fileContent], { type: getMimeType(resource) });
const blobUrl = URL.createObjectURL(blob);
console.log('blob', resource, blobUrl);
htmlContent = htmlContent.replace(
match,
`${prefix}${blobUrl}${suffix}`
);
}
console.log('htmlContent', htmlContent);
const blob = new Blob([htmlContent], { type: 'text/html' });
const iframeSrc = URL.createObjectURL(blob);
const manifestContent = await readTextFile(manifestPath);
console.log('iframeSrc', iframeSrc);
const manifest: PluginManifest = JSON.parse(manifestContent);
//const entryPath = await join(extensionDirectory, manifest.entry);
const entryPath = await join(
resourcePath,
'extension',
'demo-addon',
manifest.entry
);
console.log('extensionDirectory', extensionDirectory, entryPath);
const path = convertFileSrc(extensionDirectory, manifest.entry);
console.log('final path', path);
manifest.entry = iframeSrc;
/* await join(
path, //`file:/${extensionDirectory}`,
manifest.entry
); */
// Modul-Datei laden
//const modulePathFull = await join(basePath, manifest.main);
/* const manifest: PluginManifest = await invoke('load_plugin', {
manifestPath,
}); */
/* const iframe = document.createElement('iframe');
iframe.src = manifest.entry;
iframe.setAttribute('sandbox', 'allow-scripts');
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = 'none'; */
/* const addonApi = {
db_execute: async (sql: string, params: string[] = []) => {
return invoke('db_execute', {
addonId: manifest.name,
sql,
params,
});
},
db_select: async (sql: string, params: string[] = []) => {
return invoke('db_select', {
addonId: manifest.name,
sql,
params,
});
},
}; */
/* iframe.onload = () => {
iframe.contentWindow?.postMessage(
{ type: 'init', payload: addonApi },
'*'
);
};
window.addEventListener('message', (event) => {
if (event.source === iframe.contentWindow) {
const { type } = event.data;
if (type === 'ready') {
console.log(`Plugin ${manifest.name} ist bereit`);
}
}
}); */
/* plugins.value.push({ name: manifest.name, entry: manifest.entry });
console.log(`Plugin ${manifest.name} geladen.`); */
}
}
const extensionEntry = computedAsync(
/* const extensionEntry = computedAsync(
async () => {
try {
/* console.log("extensionEntry start", currentExtension.value);
const regex = /((href|src)=["'])([^"']+)(["'])/g; */
if (!currentExtension.value?.id || !currentExtension.value.version) {
console.log('extension id or entry missing', currentExtension.value)
@ -375,60 +365,19 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
},
null,
{ lazy: true },
)
const loadExtensionsAsync = async () => {
const { currentVault } = storeToRefs(useVaultStore())
const extensions =
(await currentVault.value?.drizzle.select().from(haexExtensions)) ?? []
//if (!extensions?.length) return false;
const installedExtensions = await filterAsync(
extensions,
isExtensionInstalledAsync,
)
console.log('loadExtensionsAsync installedExtensions', installedExtensions)
availableExtensions.value =
extensions.map((extension) => ({
id: extension.id,
name: extension.name ?? '',
icon: extension.icon ?? '',
author: extension.author ?? '',
version: extension.version ?? '',
enabled: extension.enabled ? true : false,
installed: installedExtensions.includes(extension),
})) ?? []
console.log('loadExtensionsAsync', availableExtensions.value)
return true
}
const removeExtensionAsync = async (id: string, version: string) => {
try {
console.log('remove extension', id, version)
await removeExtensionFromVaultAsync(id, version)
await removeExtensionFilesAsync(id, version)
} catch (error) {
throw new Error(JSON.stringify(error))
}
}
) */
return {
availableExtensions,
checkManifest,
currentExtension,
currentExtensionId,
extensionEntry,
extensionLinks,
installAsync,
isActive,
isExtensionInstalledAsync,
loadExtensionsAsync,
readManifestFileAsync,
removeExtensionAsync,
getExtensionPathAsync,
}
})
@ -438,7 +387,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
return 'text/plain'
} */
const removeExtensionFromVaultAsync = async (
/* const removeExtensionFromVaultAsync = async (
id: string | null,
version: string | null,
) => {
@ -457,9 +406,9 @@ const removeExtensionFromVaultAsync = async (
.delete(haexExtensions)
.where(and(eq(haexExtensions.id, id), eq(haexExtensions.version, version)))
return removedExtensions
}
} */
const removeExtensionFilesAsync = async (
/* const removeExtensionFilesAsync = async (
id: string | null,
version: string | null,
) => {
@ -483,4 +432,13 @@ const removeExtensionFilesAsync = async (
console.error('ERROR removeExtensionFilesAsync', error)
throw new Error(JSON.stringify(error))
}
} */
function encodeExtensionInfo(id: string, version: string): string {
const info = { id, version }
const jsonString = JSON.stringify(info)
const bytes = new TextEncoder().encode(jsonString)
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}

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

View File

@ -25,11 +25,10 @@ export interface IHaexHubExtensionLink extends IHaexHubExtension {
}
export interface IHaexHubExtension {
author?: string | null
enabled?: boolean | null
icon?: string | null
id: string
manifest?: IHaexHubExtensionManifest
name: string | null
version?: string | null
name: string
version: string
author: string | null
icon: string | null
enabled: boolean
}