mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 14:10:52 +01:00
refactored permission system and error handling
This commit is contained in:
21
README.md
21
README.md
@ -72,6 +72,27 @@ install:
|
|||||||
- [tauri](https://v2.tauri.app/start/prerequisites/)
|
- [tauri](https://v2.tauri.app/start/prerequisites/)
|
||||||
- [rust](https://v2.tauri.app/start/prerequisites/#rust)
|
- [rust](https://v2.tauri.app/start/prerequisites/#rust)
|
||||||
|
|
||||||
|
- install webkit2gtk + GTK3
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# debian/ubuntu
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libayatana-appindicator3-dev \
|
||||||
|
librsvg2-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# fedora
|
||||||
|
sudo dnf install \
|
||||||
|
webkit2gtk4.1-devel \
|
||||||
|
gtk3-devel \
|
||||||
|
libappindicator-gtk3 \
|
||||||
|
librsvg2-devel
|
||||||
|
```
|
||||||
|
|
||||||
- port 3003 needs to be open/free or you need to adjust it in `nuxt.config.ts` AND `src-tauri/tauri.conf.json`
|
- port 3003 needs to be open/free or you need to adjust it in `nuxt.config.ts` AND `src-tauri/tauri.conf.json`
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
"@nuxt/eslint": "1.9.0",
|
"@nuxt/eslint": "1.9.0",
|
||||||
"@nuxt/fonts": "0.11.4",
|
"@nuxt/fonts": "0.11.4",
|
||||||
"@nuxt/icon": "2.0.0",
|
"@nuxt/icon": "2.0.0",
|
||||||
"@nuxt/ui": "^3.3.2",
|
"@nuxt/ui": "4.0.0",
|
||||||
"@nuxtjs/i18n": "10.0.6",
|
"@nuxtjs/i18n": "10.0.6",
|
||||||
"@pinia/nuxt": "^0.11.1",
|
"@pinia/nuxt": "^0.11.1",
|
||||||
"@tailwindcss/vite": "^4.1.10",
|
"@tailwindcss/vite": "^4.1.10",
|
||||||
|
|||||||
351
pnpm-lock.yaml
generated
351
pnpm-lock.yaml
generated
@ -16,16 +16,16 @@ importers:
|
|||||||
version: 1.9.0(@typescript-eslint/utils@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.21)(eslint@9.35.0(jiti@2.5.1))(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
version: 1.9.0(@typescript-eslint/utils@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.21)(eslint@9.35.0(jiti@2.5.1))(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
||||||
'@nuxt/fonts':
|
'@nuxt/fonts':
|
||||||
specifier: 0.11.4
|
specifier: 0.11.4
|
||||||
version: 0.11.4(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
version: 0.11.4(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
||||||
'@nuxt/icon':
|
'@nuxt/icon':
|
||||||
specifier: 2.0.0
|
specifier: 2.0.0
|
||||||
version: 2.0.0(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
|
version: 2.0.0(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
|
||||||
'@nuxt/ui':
|
'@nuxt/ui':
|
||||||
specifier: ^3.3.2
|
specifier: 4.0.0
|
||||||
version: 3.3.3(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)
|
version: 4.0.0(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)
|
||||||
'@nuxtjs/i18n':
|
'@nuxtjs/i18n':
|
||||||
specifier: 10.0.6
|
specifier: 10.0.6
|
||||||
version: 10.0.6(@vue/compiler-dom@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(rollup@4.50.1)(vue@3.5.21(typescript@5.9.2))
|
version: 10.0.6(@vue/compiler-dom@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(rollup@4.50.1)(vue@3.5.21(typescript@5.9.2))
|
||||||
'@pinia/nuxt':
|
'@pinia/nuxt':
|
||||||
specifier: ^0.11.1
|
specifier: ^0.11.1
|
||||||
version: 0.11.2(magicast@0.3.5)(pinia@3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)))
|
version: 0.11.2(magicast@0.3.5)(pinia@3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)))
|
||||||
@ -67,10 +67,10 @@ importers:
|
|||||||
version: 13.9.0(vue@3.5.21(typescript@5.9.2))
|
version: 13.9.0(vue@3.5.21(typescript@5.9.2))
|
||||||
'@vueuse/nuxt':
|
'@vueuse/nuxt':
|
||||||
specifier: ^13.4.0
|
specifier: ^13.4.0
|
||||||
version: 13.9.0(magicast@0.3.5)(nuxt@4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(drizzle-orm@0.44.5(@libsql/client@0.15.15))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
|
version: 13.9.0(magicast@0.3.5)(nuxt@4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.44.2
|
specifier: ^0.44.2
|
||||||
version: 0.44.5(@libsql/client@0.15.15)
|
version: 0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.34.0
|
specifier: ^9.34.0
|
||||||
version: 9.35.0(jiti@2.5.1)
|
version: 9.35.0(jiti@2.5.1)
|
||||||
@ -79,7 +79,7 @@ importers:
|
|||||||
version: 7.1.0
|
version: 7.1.0
|
||||||
nuxt:
|
nuxt:
|
||||||
specifier: ^4.0.3
|
specifier: ^4.0.3
|
||||||
version: 4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(drizzle-orm@0.44.5(@libsql/client@0.15.15))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1)
|
version: 4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1)
|
||||||
nuxt-zod-i18n:
|
nuxt-zod-i18n:
|
||||||
specifier: ^1.12.0
|
specifier: ^1.12.0
|
||||||
version: 1.12.1(magicast@0.3.5)
|
version: 1.12.1(magicast@0.3.5)
|
||||||
@ -138,6 +138,34 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@ai-sdk/gateway@1.0.28':
|
||||||
|
resolution: {integrity: sha512-e9RKgWVDYHsd4UkKCgKQpK+nxLSDydN18yXctzgNlmf2R7BR+HqUsTKJdZT6ArSoXBWBGhyZss0cJJnpm6YVfw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.22.4
|
||||||
|
|
||||||
|
'@ai-sdk/provider-utils@3.0.9':
|
||||||
|
resolution: {integrity: sha512-Pm571x5efqaI4hf9yW4KsVlDBDme8++UepZRnq+kqVBWWjgvGhQlzU8glaFq0YJEB9kkxZHbRRyVeHoV2sRYaQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.22.4
|
||||||
|
|
||||||
|
'@ai-sdk/provider@2.0.0':
|
||||||
|
resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@ai-sdk/vue@2.0.51':
|
||||||
|
resolution: {integrity: sha512-pA2r/R0IMqgm7pTPfsmkXIss8g+amQ0cZfgEXq4v91FiTQedpNBcyPwHsx6rWiPV25zPGXMcXJCw+ah4TshaCw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.3.4
|
||||||
|
zod: ^3.22.4
|
||||||
|
peerDependenciesMeta:
|
||||||
|
vue:
|
||||||
|
optional: true
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0':
|
'@alloc/quick-lru@5.2.0':
|
||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -981,9 +1009,6 @@ packages:
|
|||||||
'@nuxt/fonts@0.11.4':
|
'@nuxt/fonts@0.11.4':
|
||||||
resolution: {integrity: sha512-GbLavsC+9FejVwY+KU4/wonJsKhcwOZx/eo4EuV57C4osnF/AtEmev8xqI0DNlebMEhEGZbu1MGwDDDYbeR7Bw==}
|
resolution: {integrity: sha512-GbLavsC+9FejVwY+KU4/wonJsKhcwOZx/eo4EuV57C4osnF/AtEmev8xqI0DNlebMEhEGZbu1MGwDDDYbeR7Bw==}
|
||||||
|
|
||||||
'@nuxt/icon@1.15.0':
|
|
||||||
resolution: {integrity: sha512-kA0rxqr1B601zNJNcOXera8CyYcxUCEcT7dXEC7rwAz71PRCN5emf7G656eKEQgtqrD4JSj6NQqWDgrmFcf/GQ==}
|
|
||||||
|
|
||||||
'@nuxt/icon@2.0.0':
|
'@nuxt/icon@2.0.0':
|
||||||
resolution: {integrity: sha512-sy8+zkKMYp+H09S0cuTteL3zPTmktqzYPpPXV9ZkLNjrQsaPH08n7s/9wjr+C/K/w2R3u18E3+P1VIQi3xaq1A==}
|
resolution: {integrity: sha512-sy8+zkKMYp+H09S0cuTteL3zPTmktqzYPpPXV9ZkLNjrQsaPH08n7s/9wjr+C/K/w2R3u18E3+P1VIQi3xaq1A==}
|
||||||
|
|
||||||
@ -1004,17 +1029,17 @@ packages:
|
|||||||
engines: {node: '>=18.12.0'}
|
engines: {node: '>=18.12.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@nuxt/ui@3.3.3':
|
'@nuxt/ui@4.0.0':
|
||||||
resolution: {integrity: sha512-1JS7V3FqsLQMwt6bzHYackdUtwXU/w4nRoqKLP+5WAXnsXb4nrFInLTh3wnJGsg8N6FKz2qbREimDfNuMfmKUQ==}
|
resolution: {integrity: sha512-pu5FZ8NZN2YKAiExOXuM0ImjOMe3h4/CsVgm71it+1On7OmIYHeh6SGgvaSX4Ly7FibUFllZMzJ+M5jo6KAEuw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@inertiajs/vue3': ^2.0.7
|
'@inertiajs/vue3': ^2.0.7
|
||||||
joi: ^17.13.0
|
joi: ^18.0.0
|
||||||
superstruct: ^2.0.0
|
superstruct: ^2.0.0
|
||||||
typescript: ^5.6.3
|
typescript: ^5.6.3
|
||||||
valibot: ^1.0.0
|
valibot: ^1.0.0
|
||||||
vue-router: ^4.5.0
|
vue-router: ^4.5.0
|
||||||
yup: ^1.6.0
|
yup: ^1.7.0
|
||||||
zod: ^3.22.4
|
zod: ^3.22.4
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
'@inertiajs/vue3':
|
'@inertiajs/vue3':
|
||||||
@ -1045,6 +1070,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-SQqJP6NDlmaoLzs7A74cx0Q3W4Vc+JSBlu3AN0q9+Q07Nvba5osab99GJEQ+PGnjaRwBFh35braUA2hRz9bdSA==}
|
resolution: {integrity: sha512-SQqJP6NDlmaoLzs7A74cx0Q3W4Vc+JSBlu3AN0q9+Q07Nvba5osab99GJEQ+PGnjaRwBFh35braUA2hRz9bdSA==}
|
||||||
engines: {node: '>=20.11.1'}
|
engines: {node: '>=20.11.1'}
|
||||||
|
|
||||||
|
'@opentelemetry/api@1.9.0':
|
||||||
|
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
'@oxc-minify/binding-android-arm64@0.86.0':
|
'@oxc-minify/binding-android-arm64@0.86.0':
|
||||||
resolution: {integrity: sha512-jOgbDgp6A1ax9sxHPRHBxUpxIzp2VTgbZ/6HPKIVUJ7IQqKVsELKFXIOEbCDlb1rUhZZtGf53MFypXf72kR5eQ==}
|
resolution: {integrity: sha512-jOgbDgp6A1ax9sxHPRHBxUpxIzp2VTgbZ/6HPKIVUJ7IQqKVsELKFXIOEbCDlb1rUhZZtGf53MFypXf72kR5eQ==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@ -2121,6 +2150,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: '>=3.5.18'
|
vue: '>=3.5.18'
|
||||||
|
|
||||||
|
'@unhead/vue@2.0.17':
|
||||||
|
resolution: {integrity: sha512-jzmGZYeMAhETV6qfetmLbZzUjjx1TjdNvFSobeFZb73D7dwD9wl/nOAx36qq+TvjZsLJdF5PQWToz2oDGAUqCg==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>=3.5.18'
|
||||||
|
|
||||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||||
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
|
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
@ -2446,6 +2480,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
|
ai@5.0.51:
|
||||||
|
resolution: {integrity: sha512-ToKW099QWUJNqePZbWGg8FSfxTxS3UN9U6yCla8rYdW0EBTDNPnpRwK1N6ER9TfV+dFtdUu+ZgKSlhQnEThriQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.22.4
|
||||||
|
|
||||||
ajv@6.12.6:
|
ajv@6.12.6:
|
||||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||||
|
|
||||||
@ -2837,6 +2877,15 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
debug@4.4.3:
|
||||||
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
|
engines: {node: '>=6.0'}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
|
||||||
deep-is@0.1.4:
|
deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
@ -3294,6 +3343,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
engines: {node: '>=0.8.x'}
|
engines: {node: '>=0.8.x'}
|
||||||
|
|
||||||
|
eventsource-parser@3.0.6:
|
||||||
|
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
execa@8.0.1:
|
execa@8.0.1:
|
||||||
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
||||||
engines: {node: '>=16.17'}
|
engines: {node: '>=16.17'}
|
||||||
@ -3383,6 +3436,20 @@ packages:
|
|||||||
fraction.js@4.3.7:
|
fraction.js@4.3.7:
|
||||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||||
|
|
||||||
|
framer-motion@12.23.12:
|
||||||
|
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fresh@2.0.0:
|
fresh@2.0.0:
|
||||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@ -3484,6 +3551,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
hey-listen@1.0.8:
|
||||||
|
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
|
||||||
|
|
||||||
hookable@5.5.3:
|
hookable@5.5.3:
|
||||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||||
|
|
||||||
@ -3690,6 +3760,9 @@ packages:
|
|||||||
json-schema-traverse@0.4.1:
|
json-schema-traverse@0.4.1:
|
||||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||||
|
|
||||||
|
json-schema@0.4.0:
|
||||||
|
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1:
|
json-stable-stringify-without-jsonify@1.0.1:
|
||||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||||
|
|
||||||
@ -3944,6 +4017,18 @@ packages:
|
|||||||
mocked-exports@0.1.1:
|
mocked-exports@0.1.1:
|
||||||
resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==}
|
resolution: {integrity: sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==}
|
||||||
|
|
||||||
|
motion-dom@12.23.12:
|
||||||
|
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
|
||||||
|
|
||||||
|
motion-utils@12.23.6:
|
||||||
|
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
||||||
|
|
||||||
|
motion-v@1.7.1:
|
||||||
|
resolution: {integrity: sha512-B22fYcHGx05moUtoIH0ZP/JzeacGOHzLkLmMTKU9tRB+uVMSfgqiXVzZb602qiG1ap8W7TZ+5RD5R3MmODu9oA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vueuse/core': '>=10.0.0'
|
||||||
|
vue: '>=3.0.0'
|
||||||
|
|
||||||
mrmime@2.0.1:
|
mrmime@2.0.1:
|
||||||
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -4767,6 +4852,11 @@ packages:
|
|||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
swrv@1.1.0:
|
||||||
|
resolution: {integrity: sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>=3.2.26 < 4'
|
||||||
|
|
||||||
system-architecture@0.1.0:
|
system-architecture@0.1.0:
|
||||||
resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==}
|
resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -4894,6 +4984,9 @@ packages:
|
|||||||
unhead@2.0.14:
|
unhead@2.0.14:
|
||||||
resolution: {integrity: sha512-dRP6OCqtShhMVZQe1F4wdt/WsYl2MskxKK+cvfSo0lQnrPJ4oAUQEkxRg7pPP+vJENabhlir31HwAyHUv7wfMg==}
|
resolution: {integrity: sha512-dRP6OCqtShhMVZQe1F4wdt/WsYl2MskxKK+cvfSo0lQnrPJ4oAUQEkxRg7pPP+vJENabhlir31HwAyHUv7wfMg==}
|
||||||
|
|
||||||
|
unhead@2.0.17:
|
||||||
|
resolution: {integrity: sha512-xX3PCtxaE80khRZobyWCVxeFF88/Tg9eJDcJWY9us727nsTC7C449B8BUfVBmiF2+3LjPcmqeoB2iuMs0U4oJQ==}
|
||||||
|
|
||||||
unicode-properties@1.4.1:
|
unicode-properties@1.4.1:
|
||||||
resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==}
|
resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==}
|
||||||
|
|
||||||
@ -4911,19 +5004,19 @@ packages:
|
|||||||
unifont@0.4.1:
|
unifont@0.4.1:
|
||||||
resolution: {integrity: sha512-zKSY9qO8svWYns+FGKjyVdLvpGPwqmsCjeJLN1xndMiqxHWBAhoWDMYMG960MxeV48clBmG+fDP59dHY1VoZvg==}
|
resolution: {integrity: sha512-zKSY9qO8svWYns+FGKjyVdLvpGPwqmsCjeJLN1xndMiqxHWBAhoWDMYMG960MxeV48clBmG+fDP59dHY1VoZvg==}
|
||||||
|
|
||||||
unimport@4.2.0:
|
|
||||||
resolution: {integrity: sha512-mYVtA0nmzrysnYnyb3ALMbByJ+Maosee2+WyE0puXl+Xm2bUwPorPaaeZt0ETfuroPOtG8jj1g/qeFZ6buFnag==}
|
|
||||||
engines: {node: '>=18.12.0'}
|
|
||||||
|
|
||||||
unimport@5.2.0:
|
unimport@5.2.0:
|
||||||
resolution: {integrity: sha512-bTuAMMOOqIAyjV4i4UH7P07pO+EsVxmhOzQ2YJ290J6mkLUdozNhb5I/YoOEheeNADC03ent3Qj07X0fWfUpmw==}
|
resolution: {integrity: sha512-bTuAMMOOqIAyjV4i4UH7P07pO+EsVxmhOzQ2YJ290J6mkLUdozNhb5I/YoOEheeNADC03ent3Qj07X0fWfUpmw==}
|
||||||
engines: {node: '>=18.12.0'}
|
engines: {node: '>=18.12.0'}
|
||||||
|
|
||||||
unplugin-auto-import@19.3.0:
|
unimport@5.4.0:
|
||||||
resolution: {integrity: sha512-iIi0u4Gq2uGkAOGqlPJOAMI8vocvjh1clGTfSK4SOrJKrt+tirrixo/FjgBwXQNNdS7ofcr7OxzmOb/RjWxeEQ==}
|
resolution: {integrity: sha512-g/OLFZR2mEfqbC6NC9b2225eCJGvufxq34mj6kM3OmI5gdSL0qyqtnv+9qmsGpAmnzSl6x0IWZj4W+8j2hLkMA==}
|
||||||
|
engines: {node: '>=18.12.0'}
|
||||||
|
|
||||||
|
unplugin-auto-import@20.2.0:
|
||||||
|
resolution: {integrity: sha512-vfBI/SvD9hJqYNinipVOAj5n8dS8DJXFlCKFR5iLDp2SaQwsfdnfLXgZ+34Kd3YY3YEY9omk8XQg0bwos3Q8ug==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@nuxt/kit': ^3.2.2
|
'@nuxt/kit': ^4.0.0
|
||||||
'@vueuse/core': '*'
|
'@vueuse/core': '*'
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
'@nuxt/kit':
|
'@nuxt/kit':
|
||||||
@ -4939,8 +5032,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==}
|
resolution: {integrity: sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==}
|
||||||
engines: {node: '>=20.19.0'}
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
unplugin-vue-components@28.8.0:
|
unplugin-vue-components@29.1.0:
|
||||||
resolution: {integrity: sha512-2Q6ZongpoQzuXDK0ZsVzMoshH0MWZQ1pzVL538G7oIDKRTVzHjppBDS8aB99SADGHN3lpGU7frraCG6yWNoL5Q==}
|
resolution: {integrity: sha512-z/9ACPXth199s9aCTCdKZAhe5QGOpvzJYP+Hkd0GN1/PpAmsu+W3UlRY3BJAewPqQxh5xi56+Og6mfiCV1Jzpg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/parser': ^7.15.8
|
'@babel/parser': ^7.15.8
|
||||||
@ -5226,8 +5319,8 @@ packages:
|
|||||||
vue-bundle-renderer@2.1.2:
|
vue-bundle-renderer@2.1.2:
|
||||||
resolution: {integrity: sha512-M4WRBO/O/7G9phGaGH9AOwOnYtY9ZpPoDVpBpRzR2jO5rFL9mgIlQIgums2ljCTC2HL1jDXFQc//CzWcAQHgAw==}
|
resolution: {integrity: sha512-M4WRBO/O/7G9phGaGH9AOwOnYtY9ZpPoDVpBpRzR2jO5rFL9mgIlQIgums2ljCTC2HL1jDXFQc//CzWcAQHgAw==}
|
||||||
|
|
||||||
vue-component-type-helpers@3.0.6:
|
vue-component-type-helpers@3.0.8:
|
||||||
resolution: {integrity: sha512-6CRM8X7EJqWCJOiKPvSLQG+hJPb/Oy2gyJx3pLjUEhY7PuaCthQu3e0zAGI1lqUBobrrk9IT0K8sG2GsCluxoQ==}
|
resolution: {integrity: sha512-WyR30Eq15Y/+odrUUMax6FmPbZwAp/HnC7qgR1r3lVFAcqwQ4wUoV79Mbh4SxDy3NiqDa+G4TOKD5xXSgBHo5A==}
|
||||||
|
|
||||||
vue-demi@0.14.10:
|
vue-demi@0.14.10:
|
||||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||||
@ -5388,6 +5481,32 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@ai-sdk/gateway@1.0.28(zod@3.25.76)':
|
||||||
|
dependencies:
|
||||||
|
'@ai-sdk/provider': 2.0.0
|
||||||
|
'@ai-sdk/provider-utils': 3.0.9(zod@3.25.76)
|
||||||
|
zod: 3.25.76
|
||||||
|
|
||||||
|
'@ai-sdk/provider-utils@3.0.9(zod@3.25.76)':
|
||||||
|
dependencies:
|
||||||
|
'@ai-sdk/provider': 2.0.0
|
||||||
|
'@standard-schema/spec': 1.0.0
|
||||||
|
eventsource-parser: 3.0.6
|
||||||
|
zod: 3.25.76
|
||||||
|
|
||||||
|
'@ai-sdk/provider@2.0.0':
|
||||||
|
dependencies:
|
||||||
|
json-schema: 0.4.0
|
||||||
|
|
||||||
|
'@ai-sdk/vue@2.0.51(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)':
|
||||||
|
dependencies:
|
||||||
|
'@ai-sdk/provider-utils': 3.0.9(zod@3.25.76)
|
||||||
|
ai: 5.0.51(zod@3.25.76)
|
||||||
|
swrv: 1.1.0(vue@3.5.21(typescript@5.9.2))
|
||||||
|
optionalDependencies:
|
||||||
|
vue: 3.5.21(typescript@5.9.2)
|
||||||
|
zod: 3.25.76
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
'@antfu/install-pkg@1.1.0':
|
'@antfu/install-pkg@1.1.0':
|
||||||
@ -6345,7 +6464,7 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vite
|
- vite
|
||||||
|
|
||||||
'@nuxt/fonts@0.11.4(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))':
|
'@nuxt/fonts@0.11.4(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
'@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
||||||
'@nuxt/kit': 3.19.1(magicast@0.3.5)
|
'@nuxt/kit': 3.19.1(magicast@0.3.5)
|
||||||
@ -6366,7 +6485,7 @@ snapshots:
|
|||||||
ufo: 1.6.1
|
ufo: 1.6.1
|
||||||
unifont: 0.4.1
|
unifont: 0.4.1
|
||||||
unplugin: 2.3.10
|
unplugin: 2.3.10
|
||||||
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)
|
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@azure/app-configuration'
|
- '@azure/app-configuration'
|
||||||
- '@azure/cosmos'
|
- '@azure/cosmos'
|
||||||
@ -6391,28 +6510,6 @@ snapshots:
|
|||||||
- uploadthing
|
- uploadthing
|
||||||
- vite
|
- vite
|
||||||
|
|
||||||
'@nuxt/icon@1.15.0(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
|
|
||||||
dependencies:
|
|
||||||
'@iconify/collections': 1.0.592
|
|
||||||
'@iconify/types': 2.0.0
|
|
||||||
'@iconify/utils': 2.3.0
|
|
||||||
'@iconify/vue': 5.0.0(vue@3.5.21(typescript@5.9.2))
|
|
||||||
'@nuxt/devtools-kit': 2.6.3(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
|
||||||
'@nuxt/kit': 3.19.1(magicast@0.3.5)
|
|
||||||
consola: 3.4.2
|
|
||||||
local-pkg: 1.1.2
|
|
||||||
mlly: 1.8.0
|
|
||||||
ohash: 2.0.11
|
|
||||||
pathe: 2.0.3
|
|
||||||
picomatch: 4.0.3
|
|
||||||
std-env: 3.9.0
|
|
||||||
tinyglobby: 0.2.15
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- magicast
|
|
||||||
- supports-color
|
|
||||||
- vite
|
|
||||||
- vue
|
|
||||||
|
|
||||||
'@nuxt/icon@2.0.0(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
|
'@nuxt/icon@2.0.0(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/collections': 1.0.592
|
'@iconify/collections': 1.0.592
|
||||||
@ -6516,13 +6613,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
|
|
||||||
'@nuxt/ui@3.3.3(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)':
|
'@nuxt/ui@4.0.0(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(embla-carousel@8.6.0)(ioredis@5.7.0)(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@ai-sdk/vue': 2.0.51(vue@3.5.21(typescript@5.9.2))(zod@3.25.76)
|
||||||
'@iconify/vue': 5.0.0(vue@3.5.21(typescript@5.9.2))
|
'@iconify/vue': 5.0.0(vue@3.5.21(typescript@5.9.2))
|
||||||
'@internationalized/date': 3.9.0
|
'@internationalized/date': 3.9.0
|
||||||
'@internationalized/number': 3.6.5
|
'@internationalized/number': 3.6.5
|
||||||
'@nuxt/fonts': 0.11.4(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
'@nuxt/fonts': 0.11.4(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
||||||
'@nuxt/icon': 1.15.0(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
|
'@nuxt/icon': 2.0.0(magicast@0.3.5)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
|
||||||
'@nuxt/kit': 4.1.1(magicast@0.3.5)
|
'@nuxt/kit': 4.1.1(magicast@0.3.5)
|
||||||
'@nuxt/schema': 4.1.1
|
'@nuxt/schema': 4.1.1
|
||||||
'@nuxtjs/color-mode': 3.5.2(magicast@0.3.5)
|
'@nuxtjs/color-mode': 3.5.2(magicast@0.3.5)
|
||||||
@ -6530,7 +6628,7 @@ snapshots:
|
|||||||
'@tailwindcss/postcss': 4.1.13
|
'@tailwindcss/postcss': 4.1.13
|
||||||
'@tailwindcss/vite': 4.1.13(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
'@tailwindcss/vite': 4.1.13(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
||||||
'@tanstack/vue-table': 8.21.3(vue@3.5.21(typescript@5.9.2))
|
'@tanstack/vue-table': 8.21.3(vue@3.5.21(typescript@5.9.2))
|
||||||
'@unhead/vue': 2.0.14(vue@3.5.21(typescript@5.9.2))
|
'@unhead/vue': 2.0.17(vue@3.5.21(typescript@5.9.2))
|
||||||
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
|
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
|
||||||
'@vueuse/integrations': 13.9.0(change-case@5.4.4)(fuse.js@7.1.0)(vue@3.5.21(typescript@5.9.2))
|
'@vueuse/integrations': 13.9.0(change-case@5.4.4)(fuse.js@7.1.0)(vue@3.5.21(typescript@5.9.2))
|
||||||
colortranslator: 5.0.0
|
colortranslator: 5.0.0
|
||||||
@ -6548,6 +6646,7 @@ snapshots:
|
|||||||
knitwork: 1.2.0
|
knitwork: 1.2.0
|
||||||
magic-string: 0.30.19
|
magic-string: 0.30.19
|
||||||
mlly: 1.8.0
|
mlly: 1.8.0
|
||||||
|
motion-v: 1.7.1(@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))
|
||||||
ohash: 2.0.11
|
ohash: 2.0.11
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
reka-ui: 2.5.0(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))
|
reka-ui: 2.5.0(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))
|
||||||
@ -6558,10 +6657,10 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
typescript: 5.9.2
|
typescript: 5.9.2
|
||||||
unplugin: 2.3.10
|
unplugin: 2.3.10
|
||||||
unplugin-auto-import: 19.3.0(@nuxt/kit@4.1.1(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2)))
|
unplugin-auto-import: 20.2.0(@nuxt/kit@4.1.1(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2)))
|
||||||
unplugin-vue-components: 28.8.0(@babel/parser@7.28.4)(@nuxt/kit@4.1.1(magicast@0.3.5))(vue@3.5.21(typescript@5.9.2))
|
unplugin-vue-components: 29.1.0(@babel/parser@7.28.4)(@nuxt/kit@4.1.1(magicast@0.3.5))(vue@3.5.21(typescript@5.9.2))
|
||||||
vaul-vue: 0.4.1(reka-ui@2.5.0(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))
|
vaul-vue: 0.4.1(reka-ui@2.5.0(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))
|
||||||
vue-component-type-helpers: 3.0.6
|
vue-component-type-helpers: 3.0.8
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vue-router: 4.5.1(vue@3.5.21(typescript@5.9.2))
|
vue-router: 4.5.1(vue@3.5.21(typescript@5.9.2))
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
@ -6575,6 +6674,7 @@ snapshots:
|
|||||||
- '@babel/parser'
|
- '@babel/parser'
|
||||||
- '@capacitor/preferences'
|
- '@capacitor/preferences'
|
||||||
- '@deno/kv'
|
- '@deno/kv'
|
||||||
|
- '@emotion/is-prop-valid'
|
||||||
- '@netlify/blobs'
|
- '@netlify/blobs'
|
||||||
- '@planetscale/database'
|
- '@planetscale/database'
|
||||||
- '@upstash/redis'
|
- '@upstash/redis'
|
||||||
@ -6597,6 +6697,8 @@ snapshots:
|
|||||||
- magicast
|
- magicast
|
||||||
- nprogress
|
- nprogress
|
||||||
- qrcode
|
- qrcode
|
||||||
|
- react
|
||||||
|
- react-dom
|
||||||
- sortablejs
|
- sortablejs
|
||||||
- supports-color
|
- supports-color
|
||||||
- universal-cookie
|
- universal-cookie
|
||||||
@ -6670,7 +6772,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
|
|
||||||
'@nuxtjs/i18n@10.0.6(@vue/compiler-dom@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(rollup@4.50.1)(vue@3.5.21(typescript@5.9.2))':
|
'@nuxtjs/i18n@10.0.6(@vue/compiler-dom@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(magicast@0.3.5)(rollup@4.50.1)(vue@3.5.21(typescript@5.9.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/core': 11.1.12
|
'@intlify/core': 11.1.12
|
||||||
'@intlify/h3': 0.7.1
|
'@intlify/h3': 0.7.1
|
||||||
@ -6697,7 +6799,7 @@ snapshots:
|
|||||||
ufo: 1.6.1
|
ufo: 1.6.1
|
||||||
unplugin: 2.3.10
|
unplugin: 2.3.10
|
||||||
unplugin-vue-router: 0.14.0(@vue/compiler-sfc@3.5.21)(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))
|
unplugin-vue-router: 0.14.0(@vue/compiler-sfc@3.5.21)(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))
|
||||||
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)
|
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)
|
||||||
vue-i18n: 11.1.12(vue@3.5.21(typescript@5.9.2))
|
vue-i18n: 11.1.12(vue@3.5.21(typescript@5.9.2))
|
||||||
vue-router: 4.5.1(vue@3.5.21(typescript@5.9.2))
|
vue-router: 4.5.1(vue@3.5.21(typescript@5.9.2))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -6728,6 +6830,8 @@ snapshots:
|
|||||||
- uploadthing
|
- uploadthing
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@opentelemetry/api@1.9.0': {}
|
||||||
|
|
||||||
'@oxc-minify/binding-android-arm64@0.86.0':
|
'@oxc-minify/binding-android-arm64@0.86.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -7516,6 +7620,12 @@ snapshots:
|
|||||||
unhead: 2.0.14
|
unhead: 2.0.14
|
||||||
vue: 3.5.21(typescript@5.9.2)
|
vue: 3.5.21(typescript@5.9.2)
|
||||||
|
|
||||||
|
'@unhead/vue@2.0.17(vue@3.5.21(typescript@5.9.2))':
|
||||||
|
dependencies:
|
||||||
|
hookable: 5.5.3
|
||||||
|
unhead: 2.0.17
|
||||||
|
vue: 3.5.21(typescript@5.9.2)
|
||||||
|
|
||||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -7830,13 +7940,13 @@ snapshots:
|
|||||||
|
|
||||||
'@vueuse/metadata@13.9.0': {}
|
'@vueuse/metadata@13.9.0': {}
|
||||||
|
|
||||||
'@vueuse/nuxt@13.9.0(magicast@0.3.5)(nuxt@4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(drizzle-orm@0.44.5(@libsql/client@0.15.15))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
|
'@vueuse/nuxt@13.9.0(magicast@0.3.5)(nuxt@4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 3.19.1(magicast@0.3.5)
|
'@nuxt/kit': 3.19.1(magicast@0.3.5)
|
||||||
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
|
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
|
||||||
'@vueuse/metadata': 13.9.0
|
'@vueuse/metadata': 13.9.0
|
||||||
local-pkg: 1.1.2
|
local-pkg: 1.1.2
|
||||||
nuxt: 4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(drizzle-orm@0.44.5(@libsql/client@0.15.15))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1)
|
nuxt: 4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1)
|
||||||
vue: 3.5.21(typescript@5.9.2)
|
vue: 3.5.21(typescript@5.9.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
@ -7876,6 +7986,14 @@ snapshots:
|
|||||||
|
|
||||||
agent-base@7.1.4: {}
|
agent-base@7.1.4: {}
|
||||||
|
|
||||||
|
ai@5.0.51(zod@3.25.76):
|
||||||
|
dependencies:
|
||||||
|
'@ai-sdk/gateway': 1.0.28(zod@3.25.76)
|
||||||
|
'@ai-sdk/provider': 2.0.0
|
||||||
|
'@ai-sdk/provider-utils': 3.0.9(zod@3.25.76)
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
zod: 3.25.76
|
||||||
|
|
||||||
ajv@6.12.6:
|
ajv@6.12.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
@ -8257,10 +8375,10 @@ snapshots:
|
|||||||
|
|
||||||
data-uri-to-buffer@4.0.1: {}
|
data-uri-to-buffer@4.0.1: {}
|
||||||
|
|
||||||
db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)):
|
db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@libsql/client': 0.15.15
|
'@libsql/client': 0.15.15
|
||||||
drizzle-orm: 0.44.5(@libsql/client@0.15.15)
|
drizzle-orm: 0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)
|
||||||
|
|
||||||
de-indent@1.0.2: {}
|
de-indent@1.0.2: {}
|
||||||
|
|
||||||
@ -8268,6 +8386,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
debug@4.4.3:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.3
|
||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
@ -8338,9 +8460,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
drizzle-orm@0.44.5(@libsql/client@0.15.15):
|
drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@libsql/client': 0.15.15
|
'@libsql/client': 0.15.15
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
|
||||||
duplexer@0.1.2: {}
|
duplexer@0.1.2: {}
|
||||||
|
|
||||||
@ -8692,6 +8815,8 @@ snapshots:
|
|||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
|
|
||||||
|
eventsource-parser@3.0.6: {}
|
||||||
|
|
||||||
execa@8.0.1:
|
execa@8.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
@ -8803,6 +8928,12 @@ snapshots:
|
|||||||
|
|
||||||
fraction.js@4.3.7: {}
|
fraction.js@4.3.7: {}
|
||||||
|
|
||||||
|
framer-motion@12.23.12:
|
||||||
|
dependencies:
|
||||||
|
motion-dom: 12.23.12
|
||||||
|
motion-utils: 12.23.6
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
@ -8906,6 +9037,8 @@ snapshots:
|
|||||||
|
|
||||||
he@1.2.0: {}
|
he@1.2.0: {}
|
||||||
|
|
||||||
|
hey-listen@1.0.8: {}
|
||||||
|
|
||||||
hookable@5.5.3: {}
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
http-errors@2.0.0:
|
http-errors@2.0.0:
|
||||||
@ -9080,6 +9213,8 @@ snapshots:
|
|||||||
|
|
||||||
json-schema-traverse@0.4.1: {}
|
json-schema-traverse@0.4.1: {}
|
||||||
|
|
||||||
|
json-schema@0.4.0: {}
|
||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
@ -9322,6 +9457,24 @@ snapshots:
|
|||||||
|
|
||||||
mocked-exports@0.1.1: {}
|
mocked-exports@0.1.1: {}
|
||||||
|
|
||||||
|
motion-dom@12.23.12:
|
||||||
|
dependencies:
|
||||||
|
motion-utils: 12.23.6
|
||||||
|
|
||||||
|
motion-utils@12.23.6: {}
|
||||||
|
|
||||||
|
motion-v@1.7.1(@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2)):
|
||||||
|
dependencies:
|
||||||
|
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
|
||||||
|
framer-motion: 12.23.12
|
||||||
|
hey-listen: 1.0.8
|
||||||
|
motion-dom: 12.23.12
|
||||||
|
vue: 3.5.21(typescript@5.9.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@emotion/is-prop-valid'
|
||||||
|
- react
|
||||||
|
- react-dom
|
||||||
|
|
||||||
mrmime@2.0.1: {}
|
mrmime@2.0.1: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
@ -9338,7 +9491,7 @@ snapshots:
|
|||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
nitropack@2.12.5(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)):
|
nitropack@2.12.5(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cloudflare/kv-asset-handler': 0.4.0
|
'@cloudflare/kv-asset-handler': 0.4.0
|
||||||
'@rollup/plugin-alias': 5.1.1(rollup@4.50.1)
|
'@rollup/plugin-alias': 5.1.1(rollup@4.50.1)
|
||||||
@ -9359,7 +9512,7 @@ snapshots:
|
|||||||
cookie-es: 2.0.0
|
cookie-es: 2.0.0
|
||||||
croner: 9.1.0
|
croner: 9.1.0
|
||||||
crossws: 0.3.5
|
crossws: 0.3.5
|
||||||
db0: 0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15))
|
db0: 0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
destr: 2.0.5
|
destr: 2.0.5
|
||||||
dot-prop: 9.0.0
|
dot-prop: 9.0.0
|
||||||
@ -9405,7 +9558,7 @@ snapshots:
|
|||||||
unenv: 2.0.0-rc.20
|
unenv: 2.0.0-rc.20
|
||||||
unimport: 5.2.0
|
unimport: 5.2.0
|
||||||
unplugin-utils: 0.3.0
|
unplugin-utils: 0.3.0
|
||||||
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)
|
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)
|
||||||
untyped: 2.0.0
|
untyped: 2.0.0
|
||||||
unwasm: 0.3.11
|
unwasm: 0.3.11
|
||||||
youch: 4.1.0-beta.8
|
youch: 4.1.0-beta.8
|
||||||
@ -9493,7 +9646,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
|
|
||||||
nuxt@4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(drizzle-orm@0.44.5(@libsql/client@0.15.15))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1):
|
nuxt@4.1.1(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@24.5.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))(eslint@9.35.0(jiti@2.5.1))(ioredis@5.7.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.1)(terser@5.44.0)(typescript@5.9.2)(vite@7.1.3(@types/node@24.5.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.0.6(typescript@5.9.2))(yaml@2.8.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/cli': 3.28.0(magicast@0.3.5)
|
'@nuxt/cli': 3.28.0(magicast@0.3.5)
|
||||||
'@nuxt/devalue': 2.0.2
|
'@nuxt/devalue': 2.0.2
|
||||||
@ -9528,7 +9681,7 @@ snapshots:
|
|||||||
mlly: 1.8.0
|
mlly: 1.8.0
|
||||||
mocked-exports: 0.1.1
|
mocked-exports: 0.1.1
|
||||||
nanotar: 0.2.0
|
nanotar: 0.2.0
|
||||||
nitropack: 2.12.5(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15))
|
nitropack: 2.12.5(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))
|
||||||
nypm: 0.6.1
|
nypm: 0.6.1
|
||||||
ofetch: 1.4.1
|
ofetch: 1.4.1
|
||||||
ohash: 2.0.11
|
ohash: 2.0.11
|
||||||
@ -9552,7 +9705,7 @@ snapshots:
|
|||||||
unimport: 5.2.0
|
unimport: 5.2.0
|
||||||
unplugin: 2.3.10
|
unplugin: 2.3.10
|
||||||
unplugin-vue-router: 0.15.0(@vue/compiler-sfc@3.5.21)(typescript@5.9.2)(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))
|
unplugin-vue-router: 0.15.0(@vue/compiler-sfc@3.5.21)(typescript@5.9.2)(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))
|
||||||
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0)
|
unstorage: 1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0)
|
||||||
untyped: 2.0.0
|
untyped: 2.0.0
|
||||||
vue: 3.5.21(typescript@5.9.2)
|
vue: 3.5.21(typescript@5.9.2)
|
||||||
vue-bundle-renderer: 2.1.2
|
vue-bundle-renderer: 2.1.2
|
||||||
@ -10392,6 +10545,10 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
sax: 1.4.1
|
sax: 1.4.1
|
||||||
|
|
||||||
|
swrv@1.1.0(vue@3.5.21(typescript@5.9.2)):
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.21(typescript@5.9.2)
|
||||||
|
|
||||||
system-architecture@0.1.0: {}
|
system-architecture@0.1.0: {}
|
||||||
|
|
||||||
tailwind-merge@3.3.1: {}
|
tailwind-merge@3.3.1: {}
|
||||||
@ -10513,6 +10670,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
hookable: 5.5.3
|
hookable: 5.5.3
|
||||||
|
|
||||||
|
unhead@2.0.17:
|
||||||
|
dependencies:
|
||||||
|
hookable: 5.5.3
|
||||||
|
|
||||||
unicode-properties@1.4.1:
|
unicode-properties@1.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
@ -10532,23 +10693,6 @@ snapshots:
|
|||||||
css-tree: 3.1.0
|
css-tree: 3.1.0
|
||||||
ohash: 2.0.11
|
ohash: 2.0.11
|
||||||
|
|
||||||
unimport@4.2.0:
|
|
||||||
dependencies:
|
|
||||||
acorn: 8.15.0
|
|
||||||
escape-string-regexp: 5.0.0
|
|
||||||
estree-walker: 3.0.3
|
|
||||||
local-pkg: 1.1.2
|
|
||||||
magic-string: 0.30.19
|
|
||||||
mlly: 1.8.0
|
|
||||||
pathe: 2.0.3
|
|
||||||
picomatch: 4.0.3
|
|
||||||
pkg-types: 2.3.0
|
|
||||||
scule: 1.3.0
|
|
||||||
strip-literal: 3.0.0
|
|
||||||
tinyglobby: 0.2.15
|
|
||||||
unplugin: 2.3.10
|
|
||||||
unplugin-utils: 0.2.5
|
|
||||||
|
|
||||||
unimport@5.2.0:
|
unimport@5.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
@ -10566,14 +10710,31 @@ snapshots:
|
|||||||
unplugin: 2.3.10
|
unplugin: 2.3.10
|
||||||
unplugin-utils: 0.2.5
|
unplugin-utils: 0.2.5
|
||||||
|
|
||||||
unplugin-auto-import@19.3.0(@nuxt/kit@4.1.1(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2))):
|
unimport@5.4.0:
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.15.0
|
||||||
|
escape-string-regexp: 5.0.0
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
local-pkg: 1.1.2
|
||||||
|
magic-string: 0.30.19
|
||||||
|
mlly: 1.8.0
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.3
|
||||||
|
pkg-types: 2.3.0
|
||||||
|
scule: 1.3.0
|
||||||
|
strip-literal: 3.0.0
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
unplugin: 2.3.10
|
||||||
|
unplugin-utils: 0.3.0
|
||||||
|
|
||||||
|
unplugin-auto-import@20.2.0(@nuxt/kit@4.1.1(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.21(typescript@5.9.2))):
|
||||||
dependencies:
|
dependencies:
|
||||||
local-pkg: 1.1.2
|
local-pkg: 1.1.2
|
||||||
magic-string: 0.30.19
|
magic-string: 0.30.19
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
unimport: 4.2.0
|
unimport: 5.4.0
|
||||||
unplugin: 2.3.10
|
unplugin: 2.3.10
|
||||||
unplugin-utils: 0.2.5
|
unplugin-utils: 0.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@nuxt/kit': 4.1.1(magicast@0.3.5)
|
'@nuxt/kit': 4.1.1(magicast@0.3.5)
|
||||||
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
|
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
|
||||||
@ -10588,16 +10749,16 @@ snapshots:
|
|||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
unplugin-vue-components@28.8.0(@babel/parser@7.28.4)(@nuxt/kit@4.1.1(magicast@0.3.5))(vue@3.5.21(typescript@5.9.2)):
|
unplugin-vue-components@29.1.0(@babel/parser@7.28.4)(@nuxt/kit@4.1.1(magicast@0.3.5))(vue@3.5.21(typescript@5.9.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 3.6.0
|
chokidar: 3.6.0
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
local-pkg: 1.1.2
|
local-pkg: 1.1.2
|
||||||
magic-string: 0.30.19
|
magic-string: 0.30.19
|
||||||
mlly: 1.8.0
|
mlly: 1.8.0
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
unplugin: 2.3.10
|
unplugin: 2.3.10
|
||||||
unplugin-utils: 0.2.5
|
unplugin-utils: 0.3.0
|
||||||
vue: 3.5.21(typescript@5.9.2)
|
vue: 3.5.21(typescript@5.9.2)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/parser': 7.28.4
|
'@babel/parser': 7.28.4
|
||||||
@ -10688,7 +10849,7 @@ snapshots:
|
|||||||
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
|
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
|
||||||
'@unrs/resolver-binding-win32-x64-msvc': 1.11.1
|
'@unrs/resolver-binding-win32-x64-msvc': 1.11.1
|
||||||
|
|
||||||
unstorage@1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)))(ioredis@5.7.0):
|
unstorage@1.17.1(db0@0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)))(ioredis@5.7.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
anymatch: 3.1.3
|
anymatch: 3.1.3
|
||||||
chokidar: 4.0.3
|
chokidar: 4.0.3
|
||||||
@ -10699,7 +10860,7 @@ snapshots:
|
|||||||
ofetch: 1.4.1
|
ofetch: 1.4.1
|
||||||
ufo: 1.6.1
|
ufo: 1.6.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
db0: 0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15))
|
db0: 0.3.2(@libsql/client@0.15.15)(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0))
|
||||||
ioredis: 5.7.0
|
ioredis: 5.7.0
|
||||||
|
|
||||||
untun@0.1.3:
|
untun@0.1.3:
|
||||||
@ -10861,7 +11022,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ufo: 1.6.1
|
ufo: 1.6.1
|
||||||
|
|
||||||
vue-component-type-helpers@3.0.6: {}
|
vue-component-type-helpers@3.0.8: {}
|
||||||
|
|
||||||
vue-demi@0.14.10(vue@3.5.21(typescript@5.9.2)):
|
vue-demi@0.14.10(vue@3.5.21(typescript@5.9.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@ -67,14 +67,21 @@ impl HlcService {
|
|||||||
|
|
||||||
/// Factory-Funktion: Erstellt und initialisiert einen neuen HLC-Service aus einer bestehenden DB-Verbindung.
|
/// Factory-Funktion: Erstellt und initialisiert einen neuen HLC-Service aus einer bestehenden DB-Verbindung.
|
||||||
/// Dies ist die bevorzugte Methode zur Instanziierung.
|
/// Dies ist die bevorzugte Methode zur Instanziierung.
|
||||||
pub fn new_from_connection(
|
pub fn try_initialize(conn: &Connection, app_handle: &AppHandle) -> Result<Self, HlcError> {
|
||||||
conn: &Connection,
|
|
||||||
app_handle: &AppHandle,
|
|
||||||
) -> Result<Self, HlcError> {
|
|
||||||
// 1. Hole oder erstelle eine persistente Node-ID
|
// 1. Hole oder erstelle eine persistente Node-ID
|
||||||
let node_id_str = Self::get_or_create_device_id(app_handle)?;
|
let node_id_str = Self::get_or_create_device_id(app_handle)?;
|
||||||
|
|
||||||
let node_id = ID::try_from(node_id_str.as_bytes()).map_err(|e| {
|
// Parse den String in ein Uuid-Objekt.
|
||||||
|
let uuid = Uuid::parse_str(&node_id_str).map_err(|e| {
|
||||||
|
HlcError::ParseNodeId(format!(
|
||||||
|
"Stored device ID is not a valid UUID: {}. Error: {}",
|
||||||
|
node_id_str, e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Hol dir die rohen 16 Bytes und erstelle daraus die uhlc::ID.
|
||||||
|
// Das `*` dereferenziert den `&[u8; 16]` zu `[u8; 16]`, was `try_from` erwartet.
|
||||||
|
let node_id = ID::try_from(*uuid.as_bytes()).map_err(|e| {
|
||||||
HlcError::ParseNodeId(format!("Invalid node ID format from device store: {:?}", e))
|
HlcError::ParseNodeId(format!("Invalid node ID format from device store: {:?}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@ -112,6 +119,7 @@ impl HlcService {
|
|||||||
if let Some(s) = value.as_str() {
|
if let Some(s) = value.as_str() {
|
||||||
// Das ist unser Erfolgsfall. Wir haben einen &str und können
|
// Das ist unser Erfolgsfall. Wir haben einen &str und können
|
||||||
// eine Kopie davon zurückgeben.
|
// eine Kopie davon zurückgeben.
|
||||||
|
println!("Gefundene und validierte Geräte-ID: {}", s);
|
||||||
if Uuid::parse_str(s).is_ok() {
|
if Uuid::parse_str(s).is_ok() {
|
||||||
// Erfolgsfall: Der Wert ist ein String UND eine gültige UUID.
|
// Erfolgsfall: Der Wert ist ein String UND eine gültige UUID.
|
||||||
// Wir können die Funktion direkt mit dem Wert verlassen.
|
// Wir können die Funktion direkt mit dem Wert verlassen.
|
||||||
|
|||||||
@ -115,12 +115,8 @@ impl CrdtTransformer {
|
|||||||
Statement::Query(query) => self.transform_query_recursive(query),
|
Statement::Query(query) => self.transform_query_recursive(query),
|
||||||
// Fange alle anderen Fälle ab und gib einen Fehler zurück
|
// Fange alle anderen Fälle ab und gib einen Fehler zurück
|
||||||
_ => Err(DatabaseError::UnsupportedStatement {
|
_ => Err(DatabaseError::UnsupportedStatement {
|
||||||
statement_type: format!("{:?}", stmt)
|
sql: stmt.to_string(),
|
||||||
.split('(')
|
reason: "This operation only accepts SELECT statements.".to_string(),
|
||||||
.next()
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string(),
|
|
||||||
description: "This operation only accepts SELECT statements.".to_string(),
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,8 +164,8 @@ impl CrdtTransformer {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
Err(DatabaseError::UnsupportedStatement {
|
Err(DatabaseError::UnsupportedStatement {
|
||||||
statement_type: "DELETE".to_string(),
|
sql: del_stmt.to_string(),
|
||||||
description: "DELETE from non-table source or multiple tables".to_string(),
|
reason: "DELETE from non-table source or multiple tables".to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -711,15 +707,15 @@ impl CrdtTransformer {
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(DatabaseError::UnsupportedStatement {
|
return Err(DatabaseError::UnsupportedStatement {
|
||||||
statement_type: "INSERT".to_string(),
|
sql: insert_stmt.to_string(),
|
||||||
description: "INSERT with unsupported source type".to_string(),
|
reason: "INSERT with unsupported source type".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
return Err(DatabaseError::UnsupportedStatement {
|
return Err(DatabaseError::UnsupportedStatement {
|
||||||
statement_type: "INSERT".to_string(),
|
reason: "INSERT statement has no source".to_string(),
|
||||||
description: "INSERT statement has no source".to_string(),
|
sql: insert_stmt.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -740,8 +736,8 @@ impl CrdtTransformer {
|
|||||||
from[0].clone()
|
from[0].clone()
|
||||||
} else {
|
} else {
|
||||||
return Err(DatabaseError::UnsupportedStatement {
|
return Err(DatabaseError::UnsupportedStatement {
|
||||||
statement_type: "DELETE".to_string(),
|
reason: "DELETE with multiple tables not supported".to_string(),
|
||||||
description: "DELETE with multiple tables not supported".to_string(),
|
sql: stmt.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -156,9 +156,8 @@ pub fn select(
|
|||||||
|
|
||||||
// Stelle sicher, dass es eine Query ist
|
// Stelle sicher, dass es eine Query ist
|
||||||
if !matches!(statement, Statement::Query(_)) {
|
if !matches!(statement, Statement::Query(_)) {
|
||||||
return Err(DatabaseError::UnsupportedStatement {
|
return Err(DatabaseError::StatementError {
|
||||||
statement_type: "Non-Query".to_string(),
|
reason: "Only SELECT statements are allowed in select function".to_string(),
|
||||||
description: "Only SELECT statements are allowed in select function".to_string(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
// src-tauri/src/database/error.rs
|
// src-tauri/src/database/error.rs
|
||||||
|
|
||||||
|
use crate::crdt::trigger::CrdtSetupError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::crdt::trigger::CrdtSetupError;
|
|
||||||
|
|
||||||
#[derive(Error, Debug, Serialize, Deserialize, TS)]
|
#[derive(Error, Debug, Serialize, Deserialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(tag = "type", content = "details")]
|
#[serde(tag = "type", content = "details")]
|
||||||
@ -13,14 +12,21 @@ pub enum DatabaseError {
|
|||||||
/// Der SQL-Code konnte nicht geparst werden.
|
/// Der SQL-Code konnte nicht geparst werden.
|
||||||
#[error("Failed to parse SQL: {reason} - SQL: {sql}")]
|
#[error("Failed to parse SQL: {reason} - SQL: {sql}")]
|
||||||
ParseError { reason: String, sql: String },
|
ParseError { reason: String, sql: String },
|
||||||
|
|
||||||
/// Parameter-Fehler (falsche Anzahl, ungültiger Typ, etc.)
|
/// Parameter-Fehler (falsche Anzahl, ungültiger Typ, etc.)
|
||||||
#[error("Parameter error: {reason} (expected: {expected}, provided: {provided})")]
|
#[error("Parameter count mismatch: SQL has {expected} placeholders but {provided} provided. SQL Statement: {sql}")]
|
||||||
ParamError {
|
ParameterMismatchError {
|
||||||
reason: String,
|
|
||||||
expected: usize,
|
expected: usize,
|
||||||
provided: usize,
|
provided: usize,
|
||||||
|
sql: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[error("No table provided in SQL Statement: {sql}")]
|
||||||
|
NoTableError { sql: String },
|
||||||
|
|
||||||
|
#[error("Statement Error: {reason}")]
|
||||||
|
StatementError { reason: String },
|
||||||
|
|
||||||
#[error("Failed to prepare statement: {reason}")]
|
#[error("Failed to prepare statement: {reason}")]
|
||||||
PrepareError { reason: String },
|
PrepareError { reason: String },
|
||||||
|
|
||||||
@ -28,7 +34,7 @@ pub enum DatabaseError {
|
|||||||
DatabaseError { reason: String },
|
DatabaseError { reason: String },
|
||||||
|
|
||||||
/// Ein Fehler ist während der Ausführung in der Datenbank aufgetreten.
|
/// Ein Fehler ist während der Ausführung in der Datenbank aufgetreten.
|
||||||
#[error("Execution error on table {}: {} - SQL: {}", table.as_deref().unwrap_or("unknown"), reason, sql)]
|
#[error("Execution error on table {table:?}: {reason} - SQL: {sql}")]
|
||||||
ExecutionError {
|
ExecutionError {
|
||||||
sql: String,
|
sql: String,
|
||||||
reason: String,
|
reason: String,
|
||||||
@ -37,34 +43,36 @@ pub enum DatabaseError {
|
|||||||
/// Ein Fehler ist beim Verwalten der Transaktion aufgetreten.
|
/// Ein Fehler ist beim Verwalten der Transaktion aufgetreten.
|
||||||
#[error("Transaction error: {reason}")]
|
#[error("Transaction error: {reason}")]
|
||||||
TransactionError { reason: String },
|
TransactionError { reason: String },
|
||||||
|
|
||||||
/// Ein SQL-Statement wird vom Proxy nicht unterstützt.
|
/// Ein SQL-Statement wird vom Proxy nicht unterstützt.
|
||||||
#[error("Unsupported statement type '{statement_type}': {description}")]
|
#[error("Unsupported statement. '{reason}'. - SQL: {sql}")]
|
||||||
UnsupportedStatement {
|
UnsupportedStatement { reason: String, sql: String },
|
||||||
statement_type: String,
|
|
||||||
description: String,
|
|
||||||
},
|
|
||||||
/// Fehler im HLC-Service
|
/// Fehler im HLC-Service
|
||||||
#[error("HLC error: {reason}")]
|
#[error("HLC error: {reason}")]
|
||||||
HlcError { reason: String },
|
HlcError { reason: String },
|
||||||
|
|
||||||
/// Fehler beim Sperren der Datenbankverbindung
|
/// Fehler beim Sperren der Datenbankverbindung
|
||||||
#[error("Lock error: {reason}")]
|
#[error("Lock error: {reason}")]
|
||||||
LockError { reason: String },
|
LockError { reason: String },
|
||||||
|
|
||||||
/// Fehler bei der Datenbankverbindung
|
/// Fehler bei der Datenbankverbindung
|
||||||
#[error("Connection error: {reason}")]
|
#[error("Connection error: {reason}")]
|
||||||
ConnectionError { reason: String },
|
ConnectionError { reason: String },
|
||||||
|
|
||||||
/// Fehler bei der JSON-Serialisierung
|
/// Fehler bei der JSON-Serialisierung
|
||||||
#[error("Serialization error: {reason}")]
|
#[error("Serialization error: {reason}")]
|
||||||
SerializationError { reason: String },
|
SerializationError { reason: String },
|
||||||
|
|
||||||
#[error("Permission error for extension '{extension_id}': {reason} (operation: {}, resource: {})",
|
/// Permission-bezogener Fehler für Extensions
|
||||||
operation.as_deref().unwrap_or("unknown"),
|
#[error("Permission error for extension '{extension_id}': {reason} (operation: {operation:?}, resource: {resource:?})")]
|
||||||
resource.as_deref().unwrap_or("unknown"))]
|
|
||||||
PermissionError {
|
PermissionError {
|
||||||
extension_id: String,
|
extension_id: String,
|
||||||
operation: Option<String>,
|
operation: Option<String>,
|
||||||
resource: Option<String>,
|
resource: Option<String>,
|
||||||
reason: String,
|
reason: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Query error: {reason}")]
|
#[error("Query error: {reason}")]
|
||||||
QueryError { reason: String },
|
QueryError { reason: String },
|
||||||
|
|
||||||
@ -111,7 +119,43 @@ impl From<CrdtSetupError> for DatabaseError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<crate::extension::database::ExtensionDatabaseError> for DatabaseError {
|
impl DatabaseError {
|
||||||
|
/// Extract extension ID if this error is related to an extension
|
||||||
|
pub fn extension_id(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
DatabaseError::PermissionError { extension_id, .. } => Some(extension_id.as_str()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this is a permission-related error
|
||||||
|
pub fn is_permission_error(&self) -> bool {
|
||||||
|
matches!(self, DatabaseError::PermissionError { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get operation if available
|
||||||
|
pub fn operation(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
DatabaseError::PermissionError {
|
||||||
|
operation: Some(op),
|
||||||
|
..
|
||||||
|
} => Some(op.as_str()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get resource if available
|
||||||
|
pub fn resource(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
DatabaseError::PermissionError {
|
||||||
|
resource: Some(res),
|
||||||
|
..
|
||||||
|
} => Some(res.as_str()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* impl From<crate::extension::database::ExtensionDatabaseError> for DatabaseError {
|
||||||
fn from(err: crate::extension::database::ExtensionDatabaseError) -> Self {
|
fn from(err: crate::extension::database::ExtensionDatabaseError) -> Self {
|
||||||
match err {
|
match err {
|
||||||
crate::extension::database::ExtensionDatabaseError::Permission { source } => {
|
crate::extension::database::ExtensionDatabaseError::Permission { source } => {
|
||||||
@ -156,4 +200,4 @@ impl From<crate::extension::database::ExtensionDatabaseError> for DatabaseError
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
|
|||||||
@ -13,13 +13,10 @@ use tauri::{path::BaseDirectory, AppHandle, Manager, State};
|
|||||||
|
|
||||||
use crate::crdt::hlc::HlcService;
|
use crate::crdt::hlc::HlcService;
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
|
use crate::table_names::TABLE_CRDT_CONFIGS;
|
||||||
|
use crate::AppState;
|
||||||
pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>);
|
pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>);
|
||||||
|
|
||||||
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.
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn sql_select(
|
pub fn sql_select(
|
||||||
sql: String,
|
sql: String,
|
||||||
@ -166,25 +163,33 @@ pub fn create_encrypted_database(
|
|||||||
reason: format!("Fehler beim Schließen der Quelldatenbank: {}", e),
|
reason: format!("Fehler beim Schließen der Quelldatenbank: {}", e),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let new_conn = core::open_and_init_db(&path, &key, false)?;
|
initialize_session(&app_handle, &path, &key, &state)?;
|
||||||
|
|
||||||
|
/* let new_conn = core::open_and_init_db(&path, &key, false)?;
|
||||||
|
|
||||||
// Aktualisieren der Datenbankverbindung im State
|
// Aktualisieren der Datenbankverbindung im State
|
||||||
let mut db = state.db.0.lock().map_err(|e| DatabaseError::LockError {
|
let mut db = state.db.0.lock().map_err(|e| DatabaseError::LockError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
*db = Some(new_conn);
|
*db = Some(new_conn); */
|
||||||
|
|
||||||
Ok(format!("Verschlüsselte CRDT-Datenbank erstellt",))
|
Ok(format!("Verschlüsselte CRDT-Datenbank erstellt",))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_encrypted_database(
|
pub fn open_encrypted_database(
|
||||||
//app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
path: String,
|
path: String,
|
||||||
key: String,
|
key: String,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<String, DatabaseError> {
|
) -> Result<String, DatabaseError> {
|
||||||
|
if !Path::new(&path).exists() {
|
||||||
|
return Err(DatabaseError::IoError {
|
||||||
|
path,
|
||||||
|
reason: "Database file not found.".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
/* let vault_path = app_handle
|
/* let vault_path = app_handle
|
||||||
.path()
|
.path()
|
||||||
.resolve(format!("vaults/{}", path), BaseDirectory::AppLocalData)
|
.resolve(format!("vaults/{}", path), BaseDirectory::AppLocalData)
|
||||||
@ -196,12 +201,48 @@ pub fn open_encrypted_database(
|
|||||||
return Err(format!("File not found {}", path).into());
|
return Err(format!("File not found {}", path).into());
|
||||||
} */
|
} */
|
||||||
|
|
||||||
let conn = core::open_and_init_db(&path, &key, false)
|
/* let conn = core::open_and_init_db(&path, &key, false)
|
||||||
.map_err(|e| format!("Error during open: {}", e))?;
|
.map_err(|e| format!("Error during open: {}", e))?;
|
||||||
|
|
||||||
let mut db = state.db.0.lock().map_err(|e| e.to_string())?;
|
let mut db = state.db.0.lock().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
*db = Some(conn);
|
*db = Some(conn); */
|
||||||
|
|
||||||
|
initialize_session(&app_handle, &path, &key, &state)?;
|
||||||
|
|
||||||
Ok(format!("success"))
|
Ok(format!("success"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opens the DB, initializes the HLC service, and stores both in the AppState.
|
||||||
|
fn initialize_session(
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
path: &str,
|
||||||
|
key: &str,
|
||||||
|
state: &State<'_, AppState>,
|
||||||
|
) -> Result<(), DatabaseError> {
|
||||||
|
// 1. Establish the raw database connection
|
||||||
|
let conn = core::open_and_init_db(path, key, false)?;
|
||||||
|
|
||||||
|
// 2. Initialize the HLC service
|
||||||
|
let hlc_service = HlcService::try_initialize(&conn, app_handle).map_err(|e| {
|
||||||
|
// We convert the HlcError into a DatabaseError
|
||||||
|
DatabaseError::ExecutionError {
|
||||||
|
sql: "HLC Initialization".to_string(),
|
||||||
|
reason: e.to_string(),
|
||||||
|
table: Some(TABLE_CRDT_CONFIGS.to_string()),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 3. Store everything in the global AppState
|
||||||
|
let mut db_guard = state.db.0.lock().map_err(|e| DatabaseError::LockError {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
*db_guard = Some(conn);
|
||||||
|
|
||||||
|
let mut hlc_guard = state.hlc.lock().map_err(|e| DatabaseError::LockError {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
*hlc_guard = hlc_service;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +1,178 @@
|
|||||||
|
/// 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 mime;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
http::{Request, Response},
|
http::{Request, Response},
|
||||||
AppHandle, Error as TauriError, Manager, Runtime, UriSchemeContext,
|
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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct ExtensionInfo {
|
struct ExtensionInfo {
|
||||||
id: String,
|
id: String,
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
// src-tauri/src/extension/database/mod.rs
|
// src-tauri/src/extension/database/mod.rs
|
||||||
|
|
||||||
pub mod permissions;
|
pub mod permissions;
|
||||||
|
|
||||||
use crate::crdt::hlc::HlcService;
|
use crate::crdt::hlc::HlcService;
|
||||||
use crate::crdt::transformer::CrdtTransformer;
|
use crate::crdt::transformer::CrdtTransformer;
|
||||||
use crate::crdt::trigger;
|
use crate::crdt::trigger;
|
||||||
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
use crate::database::AppState;
|
use crate::extension::error::ExtensionError;
|
||||||
use permissions::{check_read_permission, check_write_permission, PermissionError};
|
use crate::AppState;
|
||||||
|
use permissions::{check_read_permission, check_write_permission};
|
||||||
use rusqlite::params_from_iter;
|
use rusqlite::params_from_iter;
|
||||||
use rusqlite::types::Value as SqlValue;
|
use rusqlite::types::Value as SqlValue;
|
||||||
use rusqlite::Transaction;
|
use rusqlite::Transaction;
|
||||||
@ -17,36 +17,6 @@ use serde_json::Value as JsonValue;
|
|||||||
use sqlparser::ast::{Statement, TableFactor, TableObject};
|
use sqlparser::ast::{Statement, TableFactor, TableObject};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
/// Combined error type für Extension-Database operations
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum ExtensionDatabaseError {
|
|
||||||
#[error("Permission denied: {source}")]
|
|
||||||
Permission {
|
|
||||||
#[from]
|
|
||||||
source: PermissionError,
|
|
||||||
},
|
|
||||||
#[error("Database error: {source}")]
|
|
||||||
Database {
|
|
||||||
#[from]
|
|
||||||
source: DatabaseError,
|
|
||||||
},
|
|
||||||
#[error("Parameter validation failed: {reason}")]
|
|
||||||
ParameterValidation { reason: String },
|
|
||||||
#[error("Statement execution failed: {reason}")]
|
|
||||||
StatementExecution { reason: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
// Für Tauri Command Serialization
|
|
||||||
impl serde::Serialize for ExtensionDatabaseError {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(&self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Führt Statements mit korrekter Parameter-Bindung aus
|
/// Führt Statements mit korrekter Parameter-Bindung aus
|
||||||
pub struct StatementExecutor<'a> {
|
pub struct StatementExecutor<'a> {
|
||||||
@ -67,30 +37,27 @@ impl<'a> StatementExecutor<'a> {
|
|||||||
&self,
|
&self,
|
||||||
statement: &Statement,
|
statement: &Statement,
|
||||||
params: &[SqlValue],
|
params: &[SqlValue],
|
||||||
) -> Result<(), ExtensionDatabaseError> {
|
) -> Result<(), DatabaseError> {
|
||||||
let sql = statement.to_string();
|
let sql = statement.to_string();
|
||||||
let expected_params = count_sql_placeholders(&sql);
|
let expected_params = count_sql_placeholders(&sql);
|
||||||
|
|
||||||
if expected_params != params.len() {
|
if expected_params != params.len() {
|
||||||
return Err(ExtensionDatabaseError::ParameterValidation {
|
return Err(DatabaseError::ParameterMismatchError {
|
||||||
reason: format!(
|
expected: expected_params,
|
||||||
"Parameter count mismatch for statement: {} (expected: {}, provided: {})",
|
provided: params.len(),
|
||||||
truncate_sql(&sql, 100),
|
sql,
|
||||||
expected_params,
|
|
||||||
params.len()
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.transaction
|
self.transaction
|
||||||
.execute(&sql, params_from_iter(params.iter()))
|
.execute(&sql, params_from_iter(params.iter()))
|
||||||
.map_err(|e| ExtensionDatabaseError::StatementExecution {
|
.map_err(|e| DatabaseError::ExecutionError {
|
||||||
reason: format!(
|
sql,
|
||||||
"Failed to execute statement on table {}: {}",
|
table: Some(
|
||||||
self.extract_table_name_from_statement(statement)
|
self.extract_table_name_from_statement(statement)
|
||||||
.unwrap_or_else(|| "unknown".to_string()),
|
.unwrap_or_else(|| "unknown".to_string()),
|
||||||
e
|
|
||||||
),
|
),
|
||||||
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -147,7 +114,7 @@ pub async fn extension_sql_execute(
|
|||||||
extension_id: String,
|
extension_id: String,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
hlc_service: State<'_, HlcService>,
|
hlc_service: State<'_, HlcService>,
|
||||||
) -> Result<Vec<String>, ExtensionDatabaseError> {
|
) -> Result<Vec<String>, ExtensionError> {
|
||||||
// Permission check
|
// Permission check
|
||||||
check_write_permission(&state.db, &extension_id, sql).await?;
|
check_write_permission(&state.db, &extension_id, sql).await?;
|
||||||
|
|
||||||
@ -208,7 +175,7 @@ pub async fn extension_sql_execute(
|
|||||||
|
|
||||||
Ok(modified_schema_tables.into_iter().collect())
|
Ok(modified_schema_tables.into_iter().collect())
|
||||||
})
|
})
|
||||||
.map_err(ExtensionDatabaseError::from)
|
.map_err(ExtensionError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -217,7 +184,7 @@ pub async fn extension_sql_select(
|
|||||||
params: Vec<JsonValue>,
|
params: Vec<JsonValue>,
|
||||||
extension_id: String,
|
extension_id: String,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<Vec<JsonValue>, ExtensionDatabaseError> {
|
) -> Result<Vec<JsonValue>, ExtensionError> {
|
||||||
// Permission check
|
// Permission check
|
||||||
check_read_permission(&state.db, &extension_id, sql).await?;
|
check_read_permission(&state.db, &extension_id, sql).await?;
|
||||||
|
|
||||||
@ -234,8 +201,13 @@ pub async fn extension_sql_select(
|
|||||||
// Validate that all statements are queries
|
// Validate that all statements are queries
|
||||||
for stmt in &ast_vec {
|
for stmt in &ast_vec {
|
||||||
if !matches!(stmt, Statement::Query(_)) {
|
if !matches!(stmt, Statement::Query(_)) {
|
||||||
return Err(ExtensionDatabaseError::StatementExecution {
|
return Err(ExtensionError::Database {
|
||||||
reason: "Only SELECT statements are allowed in extension_sql_select".to_string(),
|
source: DatabaseError::ExecutionError {
|
||||||
|
sql: sql.to_string(),
|
||||||
|
reason: "Only SELECT statements are allowed in extension_sql_select"
|
||||||
|
.to_string(),
|
||||||
|
table: None,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -285,7 +257,7 @@ pub async fn extension_sql_select(
|
|||||||
|
|
||||||
Ok(results)
|
Ok(results)
|
||||||
})
|
})
|
||||||
.map_err(ExtensionDatabaseError::from)
|
.map_err(ExtensionError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Konvertiert eine SQLite-Zeile zu JSON
|
/// Konvertiert eine SQLite-Zeile zu JSON
|
||||||
@ -309,16 +281,14 @@ fn row_to_json_value(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Validiert Parameter gegen SQL-Platzhalter
|
/// Validiert Parameter gegen SQL-Platzhalter
|
||||||
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), ExtensionDatabaseError> {
|
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {
|
||||||
let total_placeholders = count_sql_placeholders(sql);
|
let total_placeholders = count_sql_placeholders(sql);
|
||||||
|
|
||||||
if total_placeholders != params.len() {
|
if total_placeholders != params.len() {
|
||||||
return Err(ExtensionDatabaseError::ParameterValidation {
|
return Err(DatabaseError::ParameterMismatchError {
|
||||||
reason: format!(
|
expected: total_placeholders,
|
||||||
"Parameter count mismatch: SQL has {} placeholders but {} parameters provided",
|
provided: params.len(),
|
||||||
total_placeholders,
|
sql: sql.to_string(),
|
||||||
params.len()
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,52 +5,18 @@ use crate::database::core::{
|
|||||||
};
|
};
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
use crate::database::DbConnection;
|
use crate::database::DbConnection;
|
||||||
use crate::models::DbExtensionPermission;
|
use crate::extension::error::ExtensionError;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlparser::ast::{Statement, TableFactor, TableObject};
|
use sqlparser::ast::{Statement, TableFactor, TableObject};
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub enum PermissionError {
|
pub struct DbExtensionPermission {
|
||||||
#[error("Extension '{extension_id}' has no {operation} permission for {resource}: {reason}")]
|
pub id: String,
|
||||||
AccessDenied {
|
pub extension_id: String,
|
||||||
extension_id: String,
|
pub resource: String,
|
||||||
operation: String,
|
pub operation: String,
|
||||||
resource: String,
|
pub path: String,
|
||||||
reason: String,
|
|
||||||
},
|
|
||||||
#[error("Database error while checking permissions: {source}")]
|
|
||||||
Database {
|
|
||||||
#[from]
|
|
||||||
source: DatabaseError,
|
|
||||||
},
|
|
||||||
#[error("SQL parsing error: {reason}")]
|
|
||||||
SqlParse { reason: String },
|
|
||||||
#[error("Invalid SQL statement: {reason}")]
|
|
||||||
InvalidStatement { reason: String },
|
|
||||||
#[error("No SQL statement found")]
|
|
||||||
NoStatement,
|
|
||||||
#[error("Unsupported statement type for permission check")]
|
|
||||||
UnsupportedStatement,
|
|
||||||
#[error("No table specified in {statement_type} statement")]
|
|
||||||
NoTableSpecified { statement_type: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hilfsfunktion für bessere Lesbarkeit
|
|
||||||
impl PermissionError {
|
|
||||||
pub fn access_denied(
|
|
||||||
extension_id: &str,
|
|
||||||
operation: &str,
|
|
||||||
resource: &str,
|
|
||||||
reason: &str,
|
|
||||||
) -> Self {
|
|
||||||
Self::AccessDenied {
|
|
||||||
extension_id: extension_id.to_string(),
|
|
||||||
operation: operation.to_string(),
|
|
||||||
resource: resource.to_string(),
|
|
||||||
reason: reason.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prüft Leseberechtigungen für eine Extension
|
/// Prüft Leseberechtigungen für eine Extension
|
||||||
@ -58,9 +24,10 @@ pub async fn check_read_permission(
|
|||||||
connection: &DbConnection,
|
connection: &DbConnection,
|
||||||
extension_id: &str,
|
extension_id: &str,
|
||||||
sql: &str,
|
sql: &str,
|
||||||
) -> Result<(), PermissionError> {
|
) -> Result<(), ExtensionError> {
|
||||||
let statement = parse_single_statement(sql).map_err(|e| PermissionError::SqlParse {
|
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
|
sql: sql.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match statement {
|
match statement {
|
||||||
@ -68,9 +35,11 @@ pub async fn check_read_permission(
|
|||||||
let tables = extract_table_names_from_sql(&query.to_string())?;
|
let tables = extract_table_names_from_sql(&query.to_string())?;
|
||||||
check_table_permissions(connection, extension_id, &tables, "read").await
|
check_table_permissions(connection, extension_id, &tables, "read").await
|
||||||
}
|
}
|
||||||
_ => Err(PermissionError::InvalidStatement {
|
_ => Err(DatabaseError::UnsupportedStatement {
|
||||||
reason: "Only SELECT statements are allowed for read operations".to_string(),
|
reason: "Only SELECT statements are allowed for read operations".to_string(),
|
||||||
}),
|
sql: sql.to_string(),
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,9 +48,10 @@ pub async fn check_write_permission(
|
|||||||
connection: &DbConnection,
|
connection: &DbConnection,
|
||||||
extension_id: &str,
|
extension_id: &str,
|
||||||
sql: &str,
|
sql: &str,
|
||||||
) -> Result<(), PermissionError> {
|
) -> Result<(), ExtensionError> {
|
||||||
let statement = parse_single_statement(sql).map_err(|e| PermissionError::SqlParse {
|
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
|
sql: sql.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match statement {
|
match statement {
|
||||||
@ -111,38 +81,44 @@ pub async fn check_write_permission(
|
|||||||
let table_names: Vec<String> = names.iter().map(|name| name.to_string()).collect();
|
let table_names: Vec<String> = names.iter().map(|name| name.to_string()).collect();
|
||||||
check_table_permissions(connection, extension_id, &table_names, "drop").await
|
check_table_permissions(connection, extension_id, &table_names, "drop").await
|
||||||
}
|
}
|
||||||
_ => Err(PermissionError::UnsupportedStatement),
|
_ => Err(DatabaseError::UnsupportedStatement {
|
||||||
|
reason: "SQL Statement is not allowed".to_string(),
|
||||||
|
sql: sql.to_string(),
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extrahiert Tabellenname aus INSERT-Statement
|
/// Extrahiert Tabellenname aus INSERT-Statement
|
||||||
fn extract_table_name_from_insert(
|
fn extract_table_name_from_insert(
|
||||||
insert: &sqlparser::ast::Insert,
|
insert: &sqlparser::ast::Insert,
|
||||||
) -> Result<String, PermissionError> {
|
) -> Result<String, ExtensionError> {
|
||||||
match &insert.table {
|
match &insert.table {
|
||||||
TableObject::TableName(name) => Ok(name.to_string()),
|
TableObject::TableName(name) => Ok(name.to_string()),
|
||||||
_ => Err(PermissionError::NoTableSpecified {
|
_ => Err(DatabaseError::NoTableError {
|
||||||
statement_type: "INSERT".to_string(),
|
sql: insert.to_string(),
|
||||||
}),
|
}
|
||||||
|
.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extrahiert Tabellenname aus TableFactor
|
/// Extrahiert Tabellenname aus TableFactor
|
||||||
fn extract_table_name_from_table_factor(
|
fn extract_table_name_from_table_factor(
|
||||||
table_factor: &TableFactor,
|
table_factor: &TableFactor,
|
||||||
) -> Result<String, PermissionError> {
|
) -> Result<String, ExtensionError> {
|
||||||
match table_factor {
|
match table_factor {
|
||||||
TableFactor::Table { name, .. } => Ok(name.to_string()),
|
TableFactor::Table { name, .. } => Ok(name.to_string()),
|
||||||
_ => Err(PermissionError::InvalidStatement {
|
_ => Err(DatabaseError::StatementError {
|
||||||
reason: "Complex table references not supported".to_string(),
|
reason: "Complex table references not supported".to_string(),
|
||||||
}),
|
}
|
||||||
|
.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extrahiert Tabellenname aus DELETE-Statement
|
/// Extrahiert Tabellenname aus DELETE-Statement
|
||||||
fn extract_table_name_from_delete(
|
fn extract_table_name_from_delete(
|
||||||
delete: &sqlparser::ast::Delete,
|
delete: &sqlparser::ast::Delete,
|
||||||
) -> Result<String, PermissionError> {
|
) -> Result<String, ExtensionError> {
|
||||||
use sqlparser::ast::FromTable;
|
use sqlparser::ast::FromTable;
|
||||||
|
|
||||||
let table_name = match &delete.from {
|
let table_name = match &delete.from {
|
||||||
@ -152,9 +128,10 @@ fn extract_table_name_from_delete(
|
|||||||
} else if !delete.tables.is_empty() {
|
} else if !delete.tables.is_empty() {
|
||||||
delete.tables[0].to_string()
|
delete.tables[0].to_string()
|
||||||
} else {
|
} else {
|
||||||
return Err(PermissionError::NoTableSpecified {
|
return Err(DatabaseError::NoTableError {
|
||||||
statement_type: "DELETE".to_string(),
|
sql: delete.to_string(),
|
||||||
});
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -168,7 +145,7 @@ async fn check_single_table_permission(
|
|||||||
extension_id: &str,
|
extension_id: &str,
|
||||||
table_name: &str,
|
table_name: &str,
|
||||||
operation: &str,
|
operation: &str,
|
||||||
) -> Result<(), PermissionError> {
|
) -> Result<(), ExtensionError> {
|
||||||
check_table_permissions(
|
check_table_permissions(
|
||||||
connection,
|
connection,
|
||||||
extension_id,
|
extension_id,
|
||||||
@ -184,7 +161,7 @@ async fn check_table_permissions(
|
|||||||
extension_id: &str,
|
extension_id: &str,
|
||||||
table_names: &[String],
|
table_names: &[String],
|
||||||
operation: &str,
|
operation: &str,
|
||||||
) -> Result<(), PermissionError> {
|
) -> Result<(), ExtensionError> {
|
||||||
let permissions =
|
let permissions =
|
||||||
get_extension_permissions(connection, extension_id, "database", operation).await?;
|
get_extension_permissions(connection, extension_id, "database", operation).await?;
|
||||||
|
|
||||||
@ -194,11 +171,10 @@ async fn check_table_permissions(
|
|||||||
.any(|perm| perm.path.contains(table_name));
|
.any(|perm| perm.path.contains(table_name));
|
||||||
|
|
||||||
if !has_permission {
|
if !has_permission {
|
||||||
return Err(PermissionError::access_denied(
|
return Err(ExtensionError::permission_denied(
|
||||||
extension_id,
|
extension_id,
|
||||||
operation,
|
operation,
|
||||||
&format!("table '{}'", table_name),
|
&format!("table '{}'", table_name),
|
||||||
"Table not in permitted resources",
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -207,7 +183,7 @@ async fn check_table_permissions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Ruft die Berechtigungen einer Extension aus der Datenbank ab
|
/// Ruft die Berechtigungen einer Extension aus der Datenbank ab
|
||||||
async fn get_extension_permissions(
|
pub async fn get_extension_permissions(
|
||||||
connection: &DbConnection,
|
connection: &DbConnection,
|
||||||
extension_id: &str,
|
extension_id: &str,
|
||||||
resource: &str,
|
resource: &str,
|
||||||
@ -240,10 +216,7 @@ async fn get_extension_permissions(
|
|||||||
|
|
||||||
let mut permissions = Vec::new();
|
let mut permissions = Vec::new();
|
||||||
for row_result in rows {
|
for row_result in rows {
|
||||||
let permission = row_result.map_err(|e| DatabaseError::PermissionError {
|
let permission = row_result.map_err(|e| DatabaseError::DatabaseError {
|
||||||
extension_id: extension_id.to_string(),
|
|
||||||
operation: Some(operation.to_string()),
|
|
||||||
resource: Some(resource.to_string()),
|
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
permissions.push(permission);
|
permissions.push(permission);
|
||||||
@ -255,6 +228,8 @@ async fn get_extension_permissions(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::extension::error::ExtensionError;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -269,7 +244,7 @@ mod tests {
|
|||||||
fn test_parse_invalid_sql() {
|
fn test_parse_invalid_sql() {
|
||||||
let sql = "INVALID SQL";
|
let sql = "INVALID SQL";
|
||||||
let result = parse_single_statement(sql);
|
let result = parse_single_statement(sql);
|
||||||
// parse_single_statement gibt DatabaseError zurück, nicht PermissionError
|
// parse_single_statement gibt DatabaseError zurück, nicht DatabaseError
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
// Wenn du spezifischer sein möchtest, kannst du den DatabaseError-Typ prüfen:
|
// Wenn du spezifischer sein möchtest, kannst du den DatabaseError-Typ prüfen:
|
||||||
match result {
|
match result {
|
||||||
@ -284,11 +259,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
/* #[test]
|
||||||
fn test_permission_error_access_denied() {
|
fn test_permission_error_access_denied() {
|
||||||
let error = PermissionError::access_denied("ext1", "read", "table1", "not allowed");
|
let error = ExtensionError::access_denied("ext1", "read", "table1", "not allowed");
|
||||||
match error {
|
match error {
|
||||||
PermissionError::AccessDenied {
|
ExtensionError::AccessDenied {
|
||||||
extension_id,
|
extension_id,
|
||||||
operation,
|
operation,
|
||||||
resource,
|
resource,
|
||||||
@ -301,5 +276,5 @@ mod tests {
|
|||||||
}
|
}
|
||||||
_ => panic!("Expected AccessDenied error"),
|
_ => panic!("Expected AccessDenied error"),
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
}
|
}
|
||||||
|
|||||||
214
src-tauri/src/extension/error.rs
Normal file
214
src-tauri/src/extension/error.rs
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
/// src-tauri/src/extension/error.rs
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::database::error::DatabaseError;
|
||||||
|
|
||||||
|
/// Comprehensive error type for extension operations
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ExtensionError {
|
||||||
|
#[error("Security violation: {reason}")]
|
||||||
|
SecurityViolation { reason: String },
|
||||||
|
|
||||||
|
#[error("Extension not found: {id}")]
|
||||||
|
NotFound { id: String },
|
||||||
|
|
||||||
|
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
|
||||||
|
PermissionDenied {
|
||||||
|
extension_id: String,
|
||||||
|
operation: String,
|
||||||
|
resource: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("Database operation failed: {source}")]
|
||||||
|
Database {
|
||||||
|
#[from]
|
||||||
|
source: DatabaseError,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("Filesystem operation failed: {source}")]
|
||||||
|
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>>,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("Shell command failed: {reason}")]
|
||||||
|
Shell {
|
||||||
|
reason: String,
|
||||||
|
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("Serialization error: {reason}")]
|
||||||
|
SerializationError { reason: String },
|
||||||
|
|
||||||
|
#[error("Configuration error: {reason}")]
|
||||||
|
ConfigError { reason: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtensionError {
|
||||||
|
/// Convenience constructor for permission denied errors
|
||||||
|
pub fn permission_denied(extension_id: &str, operation: &str, resource: &str) -> Self {
|
||||||
|
Self::PermissionDenied {
|
||||||
|
extension_id: extension_id.to_string(),
|
||||||
|
operation: operation.to_string(),
|
||||||
|
resource: resource.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
ExtensionError::PermissionDenied { .. } | ExtensionError::SecurityViolation { .. }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl serde::Serialize for ExtensionError {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
use serde::ser::SerializeStruct;
|
||||||
|
|
||||||
|
let mut state = serializer.serialize_struct("ExtensionError", 3)?;
|
||||||
|
|
||||||
|
// 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("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 {
|
||||||
|
state.serialize_field("extension_id", &Option::<String>::None)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Tauri command serialization
|
||||||
|
impl From<serde_json::Error> for ExtensionError {
|
||||||
|
fn from(err: serde_json::Error) -> Self {
|
||||||
|
ExtensionError::SerializationError {
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
186
src-tauri/src/extension/filesystem/core.rs
Normal file
186
src-tauri/src/extension/filesystem/core.rs
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::extension::error::ExtensionError;
|
||||||
|
|
||||||
|
/// Simple filesystem permissions using path patterns with environment-style variables
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct FilesystemPermissions {
|
||||||
|
/// Read access to files and directories
|
||||||
|
/// Examples: ["$DOCUMENT/**", "$PICTURE/*.jpg", "$APPDATA/my-extension/*"]
|
||||||
|
pub read: Option<Vec<String>>,
|
||||||
|
/// Write access to files and directories (includes create/delete)
|
||||||
|
/// Examples: ["$APPDATA/my-extension/**", "$DOWNLOAD/*.pdf"]
|
||||||
|
pub write: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilesystemPermissions {
|
||||||
|
/// Helper to create common permission patterns
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
read: None,
|
||||||
|
write: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add read permission for a path pattern
|
||||||
|
pub fn add_read(&mut self, pattern: &str) {
|
||||||
|
match &mut self.read {
|
||||||
|
Some(patterns) => patterns.push(pattern.to_string()),
|
||||||
|
None => self.read = Some(vec![pattern.to_string()]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add write permission for a path pattern
|
||||||
|
pub fn add_write(&mut self, pattern: &str) {
|
||||||
|
match &mut self.write {
|
||||||
|
Some(patterns) => patterns.push(pattern.to_string()),
|
||||||
|
None => self.write = Some(vec![pattern.to_string()]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper: Add extension's own data directory permissions
|
||||||
|
pub fn extension_data(extension_id: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
read: Some(vec![format!("$APPDATA/extensions/{}/**", extension_id)]),
|
||||||
|
write: Some(vec![format!("$APPDATA/extensions/{}/**", extension_id)]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper: Add document access permissions
|
||||||
|
pub fn documents_read_only() -> Self {
|
||||||
|
Self {
|
||||||
|
read: Some(vec!["$DOCUMENT/**".to_string()]),
|
||||||
|
write: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper: Add picture access permissions
|
||||||
|
pub fn pictures_read_only() -> Self {
|
||||||
|
Self {
|
||||||
|
read: Some(vec!["$PICTURE/**".to_string()]),
|
||||||
|
write: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate all path patterns
|
||||||
|
pub fn validate(&self) -> Result<(), ExtensionError> {
|
||||||
|
if let Some(read_patterns) = &self.read {
|
||||||
|
for pattern in read_patterns {
|
||||||
|
validate_path_pattern(pattern)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(write_patterns) = &self.write {
|
||||||
|
for pattern in write_patterns {
|
||||||
|
validate_path_pattern(pattern)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a filesystem path pattern
|
||||||
|
fn validate_path_pattern(pattern: &str) -> Result<(), ExtensionError> {
|
||||||
|
if pattern.is_empty() {
|
||||||
|
return Err(ExtensionError::ValidationError {
|
||||||
|
reason: "Path pattern cannot be empty".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if pattern starts with valid base directory variable
|
||||||
|
let valid_bases = [
|
||||||
|
"$APPDATA",
|
||||||
|
"$APPCACHE",
|
||||||
|
"$APPCONFIG",
|
||||||
|
"$APPLOCALDATA",
|
||||||
|
"$APPLOG",
|
||||||
|
"$AUDIO",
|
||||||
|
"$CACHE",
|
||||||
|
"$CONFIG",
|
||||||
|
"$DATA",
|
||||||
|
"$LOCALDATA",
|
||||||
|
"$DESKTOP",
|
||||||
|
"$DOCUMENT",
|
||||||
|
"$DOWNLOAD",
|
||||||
|
"$EXECUTABLE",
|
||||||
|
"$FONT",
|
||||||
|
"$HOME",
|
||||||
|
"$PICTURE",
|
||||||
|
"$PUBLIC",
|
||||||
|
"$RUNTIME",
|
||||||
|
"$TEMPLATE",
|
||||||
|
"$VIDEO",
|
||||||
|
"$RESOURCE",
|
||||||
|
"$TEMP",
|
||||||
|
];
|
||||||
|
|
||||||
|
let starts_with_valid_base = valid_bases.iter().any(|&base| {
|
||||||
|
pattern.starts_with(base)
|
||||||
|
&& (pattern.len() == base.len() || pattern.chars().nth(base.len()) == Some('/'))
|
||||||
|
});
|
||||||
|
|
||||||
|
if !starts_with_valid_base {
|
||||||
|
return Err(ExtensionError::ValidationError {
|
||||||
|
reason: format!(
|
||||||
|
"Path pattern '{}' must start with a valid base directory: {}",
|
||||||
|
pattern,
|
||||||
|
valid_bases.join(", ")
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for path traversal attempts
|
||||||
|
if pattern.contains("../") || pattern.contains("..\\") {
|
||||||
|
return Err(ExtensionError::SecurityViolation {
|
||||||
|
reason: format!("Path traversal detected in pattern: {}", pattern),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves a path pattern to actual filesystem paths using Tauri's BaseDirectory
|
||||||
|
pub fn resolve_path_pattern(
|
||||||
|
pattern: &str,
|
||||||
|
app_handle: &tauri::AppHandle,
|
||||||
|
) -> Result<(String, String), ExtensionError> {
|
||||||
|
let (base_var, relative_path) = if let Some(slash_pos) = pattern.find('/') {
|
||||||
|
(&pattern[..slash_pos], &pattern[slash_pos + 1..])
|
||||||
|
} else {
|
||||||
|
(pattern, "")
|
||||||
|
};
|
||||||
|
|
||||||
|
let base_directory = match base_var {
|
||||||
|
"$APPDATA" => "AppData",
|
||||||
|
"$APPCACHE" => "AppCache",
|
||||||
|
"$APPCONFIG" => "AppConfig",
|
||||||
|
"$APPLOCALDATA" => "AppLocalData",
|
||||||
|
"$APPLOG" => "AppLog",
|
||||||
|
"$AUDIO" => "Audio",
|
||||||
|
"$CACHE" => "Cache",
|
||||||
|
"$CONFIG" => "Config",
|
||||||
|
"$DATA" => "Data",
|
||||||
|
"$LOCALDATA" => "LocalData",
|
||||||
|
"$DESKTOP" => "Desktop",
|
||||||
|
"$DOCUMENT" => "Document",
|
||||||
|
"$DOWNLOAD" => "Download",
|
||||||
|
"$EXECUTABLE" => "Executable",
|
||||||
|
"$FONT" => "Font",
|
||||||
|
"$HOME" => "Home",
|
||||||
|
"$PICTURE" => "Picture",
|
||||||
|
"$PUBLIC" => "Public",
|
||||||
|
"$RUNTIME" => "Runtime",
|
||||||
|
"$TEMPLATE" => "Template",
|
||||||
|
"$VIDEO" => "Video",
|
||||||
|
"$RESOURCE" => "Resource",
|
||||||
|
"$TEMP" => "Temp",
|
||||||
|
_ => {
|
||||||
|
return Err(ExtensionError::ValidationError {
|
||||||
|
reason: format!("Unknown base directory variable: {}", base_var),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((base_directory.to_string(), relative_path.to_string()))
|
||||||
|
}
|
||||||
2
src-tauri/src/extension/filesystem/mod.rs
Normal file
2
src-tauri/src/extension/filesystem/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod core;
|
||||||
|
pub mod permissions;
|
||||||
101
src-tauri/src/extension/filesystem/permissions.rs
Normal file
101
src-tauri/src/extension/filesystem/permissions.rs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::extension::error::ExtensionError;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct FilesystemPermissions {
|
||||||
|
/// Read access to files and directories
|
||||||
|
pub read: Option<Vec<FilesystemPath>>,
|
||||||
|
/// Write access to files and directories (includes create/delete)
|
||||||
|
pub write: Option<Vec<FilesystemPath>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cross-platform filesystem path specification
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct FilesystemPath {
|
||||||
|
/// The type of path (determines base directory)
|
||||||
|
pub path_type: FilesystemPathType,
|
||||||
|
/// Relative path from the base directory
|
||||||
|
pub relative_path: String,
|
||||||
|
/// Whether subdirectories are included (recursive)
|
||||||
|
pub recursive: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Platform-agnostic path types that map to appropriate directories on each OS
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub enum FilesystemPathType {
|
||||||
|
/// App's data directory ($APPDATA on Windows, ~/.local/share on Linux, etc.)
|
||||||
|
AppData,
|
||||||
|
/// App's cache directory
|
||||||
|
AppCache,
|
||||||
|
/// App's configuration directory
|
||||||
|
AppConfig,
|
||||||
|
/// User's documents directory
|
||||||
|
Documents,
|
||||||
|
/// User's pictures directory
|
||||||
|
Pictures,
|
||||||
|
/// User's downloads directory
|
||||||
|
Downloads,
|
||||||
|
/// Temporary directory
|
||||||
|
Temp,
|
||||||
|
/// Extension's own private directory (always allowed)
|
||||||
|
ExtensionData,
|
||||||
|
/// Shared data between extensions (requires special permission)
|
||||||
|
SharedData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilesystemPath {
|
||||||
|
/// Creates a new filesystem path specification
|
||||||
|
pub fn new(path_type: FilesystemPathType, relative_path: &str, recursive: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
path_type,
|
||||||
|
relative_path: relative_path.to_string(),
|
||||||
|
recursive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the path to an actual system path
|
||||||
|
/// This would be implemented in your Tauri backend
|
||||||
|
pub fn resolve_system_path(
|
||||||
|
&self,
|
||||||
|
app_handle: &tauri::AppHandle,
|
||||||
|
) -> Result<String, ExtensionError> {
|
||||||
|
/* let base_dir = match self.path_type {
|
||||||
|
FilesystemPathType::AppData => app_handle.path().app_data_dir(),
|
||||||
|
FilesystemPathType::AppCache => app_handle.path().app_cache_dir(),
|
||||||
|
FilesystemPathType::AppConfig => app_handle.path().app_config_dir(),
|
||||||
|
FilesystemPathType::Documents => app_handle.path().document_dir(),
|
||||||
|
FilesystemPathType::Pictures => app_handle.path().picture_dir(),
|
||||||
|
FilesystemPathType::Downloads => app_handle.path().download_dir(),
|
||||||
|
FilesystemPathType::Temp => app_handle.path().temp_dir(),
|
||||||
|
FilesystemPathType::ExtensionData => app_handle
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.map(|p| p.join("extensions")),
|
||||||
|
FilesystemPathType::SharedData => {
|
||||||
|
app_handle.path().app_data_dir().map(|p| p.join("shared"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to resolve base directory: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let final_path = base_dir.join(&self.relative_path);
|
||||||
|
|
||||||
|
// Security check - ensure the resolved path is still within the base directory
|
||||||
|
if let (Ok(canonical_final), Ok(canonical_base)) =
|
||||||
|
(final_path.canonicalize(), base_dir.canonicalize())
|
||||||
|
{
|
||||||
|
if !canonical_final.starts_with(&canonical_base) {
|
||||||
|
return Err(ExtensionError::SecurityViolation {
|
||||||
|
reason: format!(
|
||||||
|
"Path traversal detected: {} escapes base directory",
|
||||||
|
self.relative_path
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
Ok("".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,2 +1,5 @@
|
|||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod database;
|
pub mod database;
|
||||||
|
pub mod error;
|
||||||
|
pub mod filesystem;
|
||||||
|
pub mod permission_manager;
|
||||||
|
|||||||
297
src-tauri/src/extension/permission_manager.rs
Normal file
297
src-tauri/src/extension/permission_manager.rs
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
/// 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.path.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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,27 +3,34 @@
|
|||||||
mod crdt;
|
mod crdt;
|
||||||
mod database;
|
mod database;
|
||||||
mod extension;
|
mod extension;
|
||||||
mod models;
|
|
||||||
|
//mod models;
|
||||||
|
|
||||||
pub mod table_names {
|
pub mod table_names {
|
||||||
include!(concat!(env!("OUT_DIR"), "/tableNames.rs"));
|
include!(concat!(env!("OUT_DIR"), "/tableNames.rs"));
|
||||||
}
|
}
|
||||||
|
|
||||||
use models::ExtensionState;
|
|
||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use crate::{
|
use crate::{crdt::hlc::HlcService, database::DbConnection, extension::core::ExtensionState};
|
||||||
|
|
||||||
|
/* use crate::{
|
||||||
crdt::hlc::HlcService,
|
crdt::hlc::HlcService,
|
||||||
database::{AppState, DbConnection},
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let protocol_name = "haex-extension";
|
let protocol_name = "haex-extension";
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
/* .register_uri_scheme_protocol(protocol_name, move |context, request| {
|
.register_uri_scheme_protocol(protocol_name, move |context, request| {
|
||||||
match extension::core::extension_protocol_handler(&context, &request) {
|
match extension::core::extension_protocol_handler(&context, &request) {
|
||||||
Ok(response) => response, // Wenn der Handler Ok ist, gib die Response direkt zurück
|
Ok(response) => response, // Wenn der Handler Ok ist, gib die Response direkt zurück
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -52,7 +59,7 @@ pub fn run() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) */
|
})
|
||||||
/* .manage(database::DbConnection(Arc::new(Mutex::new(None))))
|
/* .manage(database::DbConnection(Arc::new(Mutex::new(None))))
|
||||||
.manage(crdt::hlc::HlcService::new()) */
|
.manage(crdt::hlc::HlcService::new()) */
|
||||||
.manage(AppState {
|
.manage(AppState {
|
||||||
|
|||||||
@ -1,28 +1,45 @@
|
|||||||
// models.rs
|
// src-tauri/src/models.rs
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Mutex;
|
use std::collections::HashMap;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
/* #[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct ExtensionManifest {
|
pub struct ExtensionManifest {
|
||||||
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub author: Option<String>,
|
||||||
pub entry: String,
|
pub entry: String,
|
||||||
|
pub icon: Option<String>,
|
||||||
pub permissions: ExtensionPermissions,
|
pub permissions: ExtensionPermissions,
|
||||||
|
pub homepage: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct ExtensionPermissions {
|
pub struct ExtensionPermissions {
|
||||||
pub database: Option<DatabasePermissions>,
|
pub database: Option<DatabasePermissions>,
|
||||||
pub http: Option<Vec<String>>,
|
pub http: Option<Vec<String>>,
|
||||||
pub filesystem: Option<String>,
|
pub filesystem: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
/// Enum to represent the source of an extension
|
||||||
pub struct DatabasePermissions {
|
#[derive(Debug, Clone)]
|
||||||
pub read: Option<Vec<String>>,
|
pub enum ExtensionSource {
|
||||||
pub write: Option<Vec<String>>,
|
/// Production extension installed in app data
|
||||||
pub create: Option<Vec<String>>,
|
Production { path: PathBuf, version: String },
|
||||||
}
|
/// Development mode extension with live reloading
|
||||||
|
Development {
|
||||||
|
dev_server_url: String,
|
||||||
|
manifest_path: PathBuf,
|
||||||
|
auto_reload: bool,
|
||||||
|
},
|
||||||
|
} */
|
||||||
|
|
||||||
|
/*
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ExtensionState {
|
pub struct ExtensionState {
|
||||||
pub extensions: Mutex<std::collections::HashMap<String, ExtensionManifest>>,
|
pub extensions: Mutex<std::collections::HashMap<String, ExtensionManifest>>,
|
||||||
@ -48,3 +65,43 @@ pub struct DbExtensionPermission {
|
|||||||
pub operation: String,
|
pub operation: String,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Comprehensive error type for all extension-related operations
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ExtensionError {
|
||||||
|
/// Security violation detected
|
||||||
|
#[error("Security violation: {reason}")]
|
||||||
|
SecurityViolation { reason: String },
|
||||||
|
|
||||||
|
/// Extension not found
|
||||||
|
#[error("Extension not found: {id}")]
|
||||||
|
NotFound { id: String },
|
||||||
|
|
||||||
|
/// Permission denied
|
||||||
|
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
|
||||||
|
PermissionDenied {
|
||||||
|
extension_id: String,
|
||||||
|
operation: String,
|
||||||
|
resource: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// IO error during extension operations
|
||||||
|
#[error("IO error: {source}")]
|
||||||
|
Io {
|
||||||
|
#[from]
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Error during extension manifest parsing
|
||||||
|
#[error("Manifest error: {reason}")]
|
||||||
|
ManifestError { reason: String },
|
||||||
|
|
||||||
|
/// Input validation error
|
||||||
|
#[error("Validation error: {reason}")]
|
||||||
|
ValidationError { reason: String },
|
||||||
|
|
||||||
|
/// Development server error
|
||||||
|
#[error("Dev server error: {reason}")]
|
||||||
|
DevServerError { reason: String },
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|||||||
34
src-tauri/src/models_final.rs
Normal file
34
src-tauri/src/models_final.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// models.rs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_path_pattern() {
|
||||||
|
// Valid patterns
|
||||||
|
assert!(validate_path_pattern("$PICTURE/**").is_ok());
|
||||||
|
assert!(validate_path_pattern("$DOCUMENT/myfiles/*").is_ok());
|
||||||
|
assert!(validate_path_pattern("$APPDATA/extensions/my-ext/**").is_ok());
|
||||||
|
|
||||||
|
// Invalid patterns
|
||||||
|
assert!(validate_path_pattern("").is_err());
|
||||||
|
assert!(validate_path_pattern("$INVALID/test").is_err());
|
||||||
|
assert!(validate_path_pattern("$PICTURE/../secret").is_err());
|
||||||
|
assert!(validate_path_pattern("relative/path").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filesystem_permissions() {
|
||||||
|
let mut perms = FilesystemPermissions::new();
|
||||||
|
perms.add_read("$PICTURE/**");
|
||||||
|
perms.add_write("$APPDATA/my-ext/**");
|
||||||
|
|
||||||
|
assert!(perms.validate().is_ok());
|
||||||
|
assert_eq!(perms.read.as_ref().unwrap().len(), 1);
|
||||||
|
assert_eq!(perms.write.as_ref().unwrap().len(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
441
src-tauri/src/models_new.rs
Normal file
441
src-tauri/src/models_new.rs
Normal file
@ -0,0 +1,441 @@
|
|||||||
|
// models.rs
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Mutex, Arc};
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[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 ExtensionPermissions {
|
||||||
|
pub database: Option<DatabasePermissions>,
|
||||||
|
pub http: Option<Vec<String>>,
|
||||||
|
pub filesystem: Option<FilesystemPermissions>,
|
||||||
|
pub shell: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct DatabasePermissions {
|
||||||
|
pub read: Option<Vec<String>>,
|
||||||
|
pub write: Option<Vec<String>>,
|
||||||
|
pub create: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct FilesystemPermissions {
|
||||||
|
/// Read access to files and directories
|
||||||
|
pub read: Option<Vec<FilesystemPath>>,
|
||||||
|
/// Write access to files and directories (includes create/delete)
|
||||||
|
pub write: Option<Vec<FilesystemPath>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cross-platform filesystem path specification
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct FilesystemPath {
|
||||||
|
/// The type of path (determines base directory)
|
||||||
|
pub path_type: FilesystemPathType,
|
||||||
|
/// Relative path from the base directory
|
||||||
|
pub relative_path: String,
|
||||||
|
/// Whether subdirectories are included (recursive)
|
||||||
|
pub recursive: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Platform-agnostic path types that map to appropriate directories on each OS
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub enum FilesystemPathType {
|
||||||
|
/// App's data directory ($APPDATA on Windows, ~/.local/share on Linux, etc.)
|
||||||
|
AppData,
|
||||||
|
/// App's cache directory
|
||||||
|
AppCache,
|
||||||
|
/// App's configuration directory
|
||||||
|
AppConfig,
|
||||||
|
/// User's documents directory
|
||||||
|
Documents,
|
||||||
|
/// User's pictures directory
|
||||||
|
Pictures,
|
||||||
|
/// User's downloads directory
|
||||||
|
Downloads,
|
||||||
|
/// Temporary directory
|
||||||
|
Temp,
|
||||||
|
/// Extension's own private directory (always allowed)
|
||||||
|
ExtensionData,
|
||||||
|
/// Shared data between extensions (requires special permission)
|
||||||
|
SharedData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilesystemPath {
|
||||||
|
/// Creates a new filesystem path specification
|
||||||
|
pub fn new(path_type: FilesystemPathType, relative_path: &str, recursive: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
path_type,
|
||||||
|
relative_path: relative_path.to_string(),
|
||||||
|
recursive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the path to an actual system path
|
||||||
|
/// This would be implemented in your Tauri backend
|
||||||
|
pub fn resolve_system_path(&self, app_handle: &tauri::AppHandle) -> Result<std::path::PathBuf, ExtensionError> {
|
||||||
|
let base_dir = match self.path_type {
|
||||||
|
FilesystemPathType::AppData => app_handle.path().app_data_dir(),
|
||||||
|
FilesystemPathType::AppCache => app_handle.path().app_cache_dir(),
|
||||||
|
FilesystemPathType::AppConfig => app_handle.path().app_config_dir(),
|
||||||
|
FilesystemPathType::Documents => app_handle.path().document_dir(),
|
||||||
|
FilesystemPathType::Pictures => app_handle.path().picture_dir(),
|
||||||
|
FilesystemPathType::Downloads => app_handle.path().download_dir(),
|
||||||
|
FilesystemPathType::Temp => app_handle.path().temp_dir(),
|
||||||
|
FilesystemPathType::ExtensionData => {
|
||||||
|
app_handle.path().app_data_dir().map(|p| p.join("extensions"))
|
||||||
|
},
|
||||||
|
FilesystemPathType::SharedData => {
|
||||||
|
app_handle.path().app_data_dir().map(|p| p.join("shared"))
|
||||||
|
},
|
||||||
|
}.map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to resolve base directory: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let final_path = base_dir.join(&self.relative_path);
|
||||||
|
|
||||||
|
// Security check - ensure the resolved path is still within the base directory
|
||||||
|
if let (Ok(canonical_final), Ok(canonical_base)) = (final_path.canonicalize(), base_dir.canonicalize()) {
|
||||||
|
if !canonical_final.starts_with(&canonical_base) {
|
||||||
|
return Err(ExtensionError::SecurityViolation {
|
||||||
|
reason: format!("Path traversal detected: {} escapes base directory", self.relative_path),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(final_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum to represent the source of an extension
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ExtensionSource {
|
||||||
|
/// Production extension installed in app data
|
||||||
|
Production {
|
||||||
|
path: PathBuf,
|
||||||
|
version: String,
|
||||||
|
},
|
||||||
|
/// Development mode extension with live reloading
|
||||||
|
Development {
|
||||||
|
dev_server_url: String,
|
||||||
|
manifest_path: PathBuf,
|
||||||
|
auto_reload: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete extension data including source and runtime status
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Extension {
|
||||||
|
/// Unique extension ID
|
||||||
|
pub id: String,
|
||||||
|
/// Extension display name
|
||||||
|
pub name: String,
|
||||||
|
/// Source information (production or dev)
|
||||||
|
pub source: ExtensionSource,
|
||||||
|
/// Complete manifest data
|
||||||
|
pub manifest: ExtensionManifest,
|
||||||
|
/// Enabled status
|
||||||
|
pub enabled: bool,
|
||||||
|
/// Last access timestamp
|
||||||
|
pub last_accessed: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cached permission data to avoid frequent database lookups
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CachedPermission {
|
||||||
|
/// The permissions that were fetched
|
||||||
|
pub permissions: Vec<DbExtensionPermission>,
|
||||||
|
/// When this cache entry was created
|
||||||
|
pub cached_at: SystemTime,
|
||||||
|
/// How long this cache entry is valid
|
||||||
|
pub ttl: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enhanced extension manager with production/dev support and caching
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ExtensionManager {
|
||||||
|
/// Production extensions loaded from app data directory
|
||||||
|
pub production_extensions: Mutex<HashMap<String, Extension>>,
|
||||||
|
/// Development mode extensions for live-reloading during development
|
||||||
|
pub dev_extensions: Mutex<HashMap<String, Extension>>,
|
||||||
|
/// Cache for extension permissions to improve performance
|
||||||
|
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtensionManager {
|
||||||
|
/// Creates a new extension manager
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
production_extensions: Mutex::new(HashMap::new()),
|
||||||
|
dev_extensions: Mutex::new(HashMap::new()),
|
||||||
|
permission_cache: Mutex::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a production extension to the manager
|
||||||
|
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 for production extension".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a development mode extension to the manager
|
||||||
|
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 for dev extension".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets an extension by its ID
|
||||||
|
pub fn get_extension(&self, extension_id: &str) -> Option<Extension> {
|
||||||
|
// First check development extensions (they 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 extensions
|
||||||
|
let prod_extensions = self.production_extensions.lock().unwrap();
|
||||||
|
prod_extensions.get(extension_id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes an extension from the manager
|
||||||
|
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
|
||||||
|
// Check dev extensions first
|
||||||
|
{
|
||||||
|
let mut dev_extensions = self.dev_extensions.lock().unwrap();
|
||||||
|
if dev_extensions.remove(extension_id).is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check production extensions
|
||||||
|
{
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets cached permissions or indicates they need to be loaded
|
||||||
|
pub fn get_cached_permissions(
|
||||||
|
&self,
|
||||||
|
extension_id: &str,
|
||||||
|
resource: &str,
|
||||||
|
operation: &str,
|
||||||
|
) -> Option<Vec<DbExtensionPermission>> {
|
||||||
|
let cache = self.permission_cache.lock().unwrap();
|
||||||
|
let cache_key = format!("{}-{}-{}", extension_id, resource, operation);
|
||||||
|
|
||||||
|
if let Some(cached) = cache.get(&cache_key) {
|
||||||
|
let now = SystemTime::now();
|
||||||
|
if now.duration_since(cached.cached_at).unwrap_or(Duration::from_secs(0)) < cached.ttl {
|
||||||
|
return Some(cached.permissions.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the permission cache
|
||||||
|
pub fn update_permission_cache(
|
||||||
|
&self,
|
||||||
|
extension_id: &str,
|
||||||
|
resource: &str,
|
||||||
|
operation: &str,
|
||||||
|
permissions: Vec<DbExtensionPermission>,
|
||||||
|
) {
|
||||||
|
let mut cache = self.permission_cache.lock().unwrap();
|
||||||
|
let cache_key = format!("{}-{}-{}", extension_id, resource, operation);
|
||||||
|
|
||||||
|
cache.insert(cache_key, CachedPermission {
|
||||||
|
permissions,
|
||||||
|
cached_at: SystemTime::now(),
|
||||||
|
ttl: Duration::from_secs(60), // Cache for 60 seconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a manifest for security concerns
|
||||||
|
pub fn validate_manifest_security(&self, manifest: &ExtensionManifest) -> Result<(), ExtensionError> {
|
||||||
|
// Check for suspicious permission combinations
|
||||||
|
let has_filesystem = manifest.permissions.filesystem.is_some();
|
||||||
|
let has_database = manifest.permissions.database.is_some();
|
||||||
|
let has_shell = manifest.permissions.shell.is_some();
|
||||||
|
|
||||||
|
if has_filesystem && has_database && has_shell {
|
||||||
|
// This is a powerful combination, warn or check user confirmation elsewhere
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ID format
|
||||||
|
if !manifest.id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
|
||||||
|
return Err(ExtensionError::ValidationError {
|
||||||
|
reason: "Invalid extension ID format. Must contain only alphanumeric characters, dash or underscore.".to_string()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists all enabled extensions (both dev and production)
|
||||||
|
pub fn list_enabled_extensions(&self) -> Vec<Extension> {
|
||||||
|
let mut extensions = Vec::new();
|
||||||
|
|
||||||
|
// Add enabled dev extensions first (higher priority)
|
||||||
|
{
|
||||||
|
let dev_extensions = self.dev_extensions.lock().unwrap();
|
||||||
|
extensions.extend(
|
||||||
|
dev_extensions
|
||||||
|
.values()
|
||||||
|
.filter(|ext| ext.enabled)
|
||||||
|
.cloned()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add enabled production extensions (avoiding duplicates)
|
||||||
|
{
|
||||||
|
let prod_extensions = self.production_extensions.lock().unwrap();
|
||||||
|
let dev_ids: std::collections::HashSet<String> = extensions.iter().map(|e| e.id.clone()).collect();
|
||||||
|
|
||||||
|
extensions.extend(
|
||||||
|
prod_extensions
|
||||||
|
.values()
|
||||||
|
.filter(|ext| ext.enabled && !dev_ids.contains(&ext.id))
|
||||||
|
.cloned()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For backward compatibility - will be deprecated
|
||||||
|
#[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(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct DbExtensionPermission {
|
||||||
|
pub id: String,
|
||||||
|
pub extension_id: String,
|
||||||
|
pub resource: String,
|
||||||
|
pub operation: String,
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Comprehensive error type for all extension-related operations
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ExtensionError {
|
||||||
|
/// Security violation detected
|
||||||
|
#[error("Security violation: {reason}")]
|
||||||
|
SecurityViolation {
|
||||||
|
reason: String
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Extension not found
|
||||||
|
#[error("Extension not found: {id}")]
|
||||||
|
NotFound {
|
||||||
|
id: String
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Permission denied
|
||||||
|
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
|
||||||
|
PermissionDenied {
|
||||||
|
extension_id: String,
|
||||||
|
operation: String,
|
||||||
|
resource: String
|
||||||
|
},
|
||||||
|
|
||||||
|
/// IO error during extension operations
|
||||||
|
#[error("IO error: {source}")]
|
||||||
|
Io {
|
||||||
|
#[from]
|
||||||
|
source: std::io::Error
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Error during extension manifest parsing
|
||||||
|
#[error("Manifest error: {reason}")]
|
||||||
|
ManifestError {
|
||||||
|
reason: String
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Input validation error
|
||||||
|
#[error("Validation error: {reason}")]
|
||||||
|
ValidationError {
|
||||||
|
reason: String
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Development server error
|
||||||
|
#[error("Dev server error: {reason}")]
|
||||||
|
DevServerError {
|
||||||
|
reason: String
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Tauri Command Serialization
|
||||||
|
impl serde::Serialize for ExtensionError {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/pages/vault/[vaultId]/extensions/[extensionId].vue
Normal file
34
src/pages/vault/[vaultId]/extensions/[extensionId].vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full overflow-scroll">
|
||||||
|
<div>
|
||||||
|
{{ iframeIndex }}
|
||||||
|
</div>
|
||||||
|
<iframe
|
||||||
|
v-if="iframeIndex"
|
||||||
|
ref="iFrameRef"
|
||||||
|
class="w-full h-full"
|
||||||
|
:src="iframeIndex"
|
||||||
|
sandbox="allow-scripts allow-same-origin"
|
||||||
|
allow="autoplay; speaker-selection; encrypted-media;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
name: 'haexExtension',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { extensionEntry: iframeSrc } = storeToRefs(useExtensionsStore())
|
||||||
|
|
||||||
|
const iframeIndex = computed(() =>
|
||||||
|
iframeSrc.value ? `${iframeSrc.value}/index.html` : '',
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
de:
|
||||||
|
loading: Erweiterung wird geladen
|
||||||
|
en:
|
||||||
|
loading: Extension is loading
|
||||||
|
</i18n>
|
||||||
256
src/pages/vault/[vaultId]/extensions/index.vue
Normal file
256
src/pages/vault/[vaultId]/extensions/index.vue
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col p-4 relative h-full">
|
||||||
|
<div
|
||||||
|
v-if="extensionStore.availableExtensions.length"
|
||||||
|
class="flex"
|
||||||
|
>
|
||||||
|
<UiButton
|
||||||
|
class="fixed top-20 right-4 btn-square btn-primary"
|
||||||
|
@click="prepareInstallExtensionAsync"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="mdi:plus"
|
||||||
|
size="1.5em"
|
||||||
|
/>
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<HaexExtensionCard
|
||||||
|
v-for="_extension in extensionStore.availableExtensions"
|
||||||
|
v-bind="_extension"
|
||||||
|
:key="_extension.id"
|
||||||
|
@remove="onShowRemoveDialog(_extension)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="h-full w-full"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="my-icon:extensions-overview"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HaexExtensionDialogReinstall
|
||||||
|
v-model:open="openOverwriteDialog"
|
||||||
|
:manifest="extension.manifest"
|
||||||
|
@confirm="addExtensionAsync"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HaexExtensionDialogInstall
|
||||||
|
v-model:open="showConfirmation"
|
||||||
|
:manifest="extension.manifest"
|
||||||
|
@confirm="addExtensionAsync"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HaexExtensionDialogRemove
|
||||||
|
v-model:open="showRemoveDialog"
|
||||||
|
:extension="extensionToBeRemoved"
|
||||||
|
@confirm="removeExtensionAsync"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { join } from '@tauri-apps/api/path'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { readTextFile } from '@tauri-apps/plugin-fs'
|
||||||
|
import type {
|
||||||
|
IHaexHubExtension,
|
||||||
|
IHaexHubExtensionManifest,
|
||||||
|
} from '~/types/haexhub'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
name: 'extensionOverview',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const extensionStore = useExtensionsStore()
|
||||||
|
|
||||||
|
const showConfirmation = ref(false)
|
||||||
|
const openOverwriteDialog = ref(false)
|
||||||
|
|
||||||
|
const extension = reactive<{
|
||||||
|
manifest: IHaexHubExtensionManifest | null | undefined
|
||||||
|
path: string | null
|
||||||
|
}>({
|
||||||
|
manifest: null,
|
||||||
|
path: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadExtensionManifestAsync = async () => {
|
||||||
|
try {
|
||||||
|
extension.path = await open({ directory: true, recursive: true })
|
||||||
|
if (!extension.path) return
|
||||||
|
|
||||||
|
const manifestFile = JSON.parse(
|
||||||
|
await readTextFile(await join(extension.path, 'manifest.json')),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!extensionStore.checkManifest(manifestFile))
|
||||||
|
throw new Error(`Manifest fehlerhaft ${JSON.stringify(manifestFile)}`)
|
||||||
|
|
||||||
|
return manifestFile
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler loadExtensionManifestAsync:', error)
|
||||||
|
add({ color: 'error', description: JSON.stringify(error) })
|
||||||
|
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { add } = useToast()
|
||||||
|
const { addNotificationAsync } = useNotificationStore()
|
||||||
|
|
||||||
|
const prepareInstallExtensionAsync = async () => {
|
||||||
|
try {
|
||||||
|
const manifest = await loadExtensionManifestAsync()
|
||||||
|
if (!manifest) throw new Error('No valid Manifest found')
|
||||||
|
|
||||||
|
extension.manifest = manifest
|
||||||
|
|
||||||
|
const isAlreadyInstalled = await extensionStore.isExtensionInstalledAsync({
|
||||||
|
id: manifest.id,
|
||||||
|
version: manifest.version,
|
||||||
|
})
|
||||||
|
if (isAlreadyInstalled) {
|
||||||
|
openOverwriteDialog.value = true
|
||||||
|
} else {
|
||||||
|
await addExtensionAsync()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
add({ color: 'error', description: JSON.stringify(error) })
|
||||||
|
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addExtensionAsync = async () => {
|
||||||
|
try {
|
||||||
|
await extensionStore.installAsync(extension.path)
|
||||||
|
await extensionStore.loadExtensionsAsync()
|
||||||
|
|
||||||
|
add({
|
||||||
|
color: 'success',
|
||||||
|
title: t('extension.success.title', {
|
||||||
|
extension: extension.manifest?.name,
|
||||||
|
}),
|
||||||
|
description: t('extension.success.text'),
|
||||||
|
})
|
||||||
|
await addNotificationAsync({
|
||||||
|
text: t('extension.success.text'),
|
||||||
|
type: 'success',
|
||||||
|
title: t('extension.success.title', {
|
||||||
|
extension: extension.manifest?.name,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler addExtensionAsync:', error)
|
||||||
|
add({ color: 'error', description: JSON.stringify(error) })
|
||||||
|
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showRemoveDialog = ref(false)
|
||||||
|
const extensionToBeRemoved = ref<IHaexHubExtension>()
|
||||||
|
|
||||||
|
const onShowRemoveDialog = (extension: IHaexHubExtension) => {
|
||||||
|
extensionToBeRemoved.value = extension
|
||||||
|
showRemoveDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeExtensionAsync = async () => {
|
||||||
|
if (!extensionToBeRemoved.value?.id || !extensionToBeRemoved.value?.version) {
|
||||||
|
add({
|
||||||
|
color: 'error',
|
||||||
|
description: 'Erweiterung kann nicht gelöscht werden',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await extensionStore.removeExtensionAsync(
|
||||||
|
extensionToBeRemoved.value.id,
|
||||||
|
extensionToBeRemoved.value.version,
|
||||||
|
)
|
||||||
|
await extensionStore.loadExtensionsAsync()
|
||||||
|
add({
|
||||||
|
color: 'success',
|
||||||
|
title: t('extension.remove.success.title', {
|
||||||
|
extensionName: extensionToBeRemoved.value.name,
|
||||||
|
}),
|
||||||
|
description: t('extension.remove.success.text', {
|
||||||
|
extensionName: extensionToBeRemoved.value.name,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
await addNotificationAsync({
|
||||||
|
text: t('extension.remove.success.text', {
|
||||||
|
extensionName: extensionToBeRemoved.value.name,
|
||||||
|
}),
|
||||||
|
type: 'success',
|
||||||
|
title: t('extension.remove.success.title', {
|
||||||
|
extensionName: extensionToBeRemoved.value.name,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
add({
|
||||||
|
color: 'error',
|
||||||
|
title: t('extension.remove.error.title'),
|
||||||
|
description: t('extension.remove.error.text', {
|
||||||
|
error: JSON.stringify(error),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
await addNotificationAsync({
|
||||||
|
type: 'error',
|
||||||
|
title: t('extension.remove.error.title'),
|
||||||
|
text: t('extension.remove.error.text', { error: JSON.stringify(error) }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
de:
|
||||||
|
title: 'Erweiterung installieren'
|
||||||
|
extension:
|
||||||
|
remove:
|
||||||
|
success:
|
||||||
|
text: 'Erweiterung {extensionName} wurde erfolgreich entfernt'
|
||||||
|
title: '{extensionName} entfernt'
|
||||||
|
error:
|
||||||
|
text: "Erweiterung {extensionName} konnte nicht entfernt werden. \n {error}"
|
||||||
|
title: 'Fehler beim Entfernen von {extensionName}'
|
||||||
|
|
||||||
|
add: 'Erweiterung hinzufügen'
|
||||||
|
success:
|
||||||
|
title: '{extension} hinzugefügt'
|
||||||
|
text: 'Die Erweiterung wurde erfolgreich hinzugefügt'
|
||||||
|
en:
|
||||||
|
title: 'Install extension'
|
||||||
|
extension:
|
||||||
|
remove:
|
||||||
|
success:
|
||||||
|
text: 'Extension {extensionName} was removed'
|
||||||
|
title: '{extensionName} removed'
|
||||||
|
error:
|
||||||
|
text: "Extension {extensionName} couldn't be removed. \n {error}"
|
||||||
|
title: 'Exception during uninstall {extensionName}'
|
||||||
|
|
||||||
|
add: 'Add Extension'
|
||||||
|
success:
|
||||||
|
title: '{extension} added'
|
||||||
|
text: 'Extensions was added successfully'
|
||||||
|
</i18n>
|
||||||
@ -37,13 +37,14 @@ export const usePasswordGroupStore = defineStore('passwordGroupStore', () => {
|
|||||||
) => {
|
) => {
|
||||||
const group = groups.value.find((group) => group.id === groupId)
|
const group = groups.value.find((group) => group.id === groupId)
|
||||||
console.log('getParentChain1: found group', group, chain)
|
console.log('getParentChain1: found group', group, chain)
|
||||||
if (group) {
|
/* if (group) {
|
||||||
chain.push(group)
|
chain.push(group)
|
||||||
console.log('getParentChain: found group', group, chain)
|
console.log('getParentChain: found group', group, chain)
|
||||||
return getParentChain(group.parentId, chain)
|
return getParentChain(group.parentId, chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
return chain.reverse()
|
return chain.reverse() */
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncGroupItemsAsync = async () => {
|
const syncGroupItemsAsync = async () => {
|
||||||
@ -322,7 +323,7 @@ const deleteGroupAsync = async (groupId: string, final: boolean = false) => {
|
|||||||
const items = (await readByGroupIdAsync(groupId)) ?? []
|
const items = (await readByGroupIdAsync(groupId)) ?? []
|
||||||
console.log('deleteGroupAsync delete Items', items)
|
console.log('deleteGroupAsync delete Items', items)
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
await deleteAsync(item.id, true)
|
if (item) await deleteAsync(item.id, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await currentVault?.drizzle
|
return await currentVault?.drizzle
|
||||||
|
|||||||
@ -33,7 +33,6 @@ export const usePasswordItemStore = defineStore('passwordItemStore', () => {
|
|||||||
|
|
||||||
const syncItemsAsync = async () => {
|
const syncItemsAsync = async () => {
|
||||||
const { currentVault } = useVaultStore()
|
const { currentVault } = useVaultStore()
|
||||||
|
|
||||||
items.value =
|
items.value =
|
||||||
(await currentVault?.drizzle
|
(await currentVault?.drizzle
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
Reference in New Issue
Block a user