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/)
|
||||
- [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`
|
||||
|
||||
```
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
"@nuxt/eslint": "1.9.0",
|
||||
"@nuxt/fonts": "0.11.4",
|
||||
"@nuxt/icon": "2.0.0",
|
||||
"@nuxt/ui": "^3.3.2",
|
||||
"@nuxt/ui": "4.0.0",
|
||||
"@nuxtjs/i18n": "10.0.6",
|
||||
"@pinia/nuxt": "^0.11.1",
|
||||
"@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))
|
||||
'@nuxt/fonts':
|
||||
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':
|
||||
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))
|
||||
'@nuxt/ui':
|
||||
specifier: ^3.3.2
|
||||
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)
|
||||
specifier: 4.0.0
|
||||
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':
|
||||
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':
|
||||
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)))
|
||||
@ -67,10 +67,10 @@ importers:
|
||||
version: 13.9.0(vue@3.5.21(typescript@5.9.2))
|
||||
'@vueuse/nuxt':
|
||||
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:
|
||||
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:
|
||||
specifier: ^9.34.0
|
||||
version: 9.35.0(jiti@2.5.1)
|
||||
@ -79,7 +79,7 @@ importers:
|
||||
version: 7.1.0
|
||||
nuxt:
|
||||
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:
|
||||
specifier: ^1.12.0
|
||||
version: 1.12.1(magicast@0.3.5)
|
||||
@ -138,6 +138,34 @@ importers:
|
||||
|
||||
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':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -981,9 +1009,6 @@ packages:
|
||||
'@nuxt/fonts@0.11.4':
|
||||
resolution: {integrity: sha512-GbLavsC+9FejVwY+KU4/wonJsKhcwOZx/eo4EuV57C4osnF/AtEmev8xqI0DNlebMEhEGZbu1MGwDDDYbeR7Bw==}
|
||||
|
||||
'@nuxt/icon@1.15.0':
|
||||
resolution: {integrity: sha512-kA0rxqr1B601zNJNcOXera8CyYcxUCEcT7dXEC7rwAz71PRCN5emf7G656eKEQgtqrD4JSj6NQqWDgrmFcf/GQ==}
|
||||
|
||||
'@nuxt/icon@2.0.0':
|
||||
resolution: {integrity: sha512-sy8+zkKMYp+H09S0cuTteL3zPTmktqzYPpPXV9ZkLNjrQsaPH08n7s/9wjr+C/K/w2R3u18E3+P1VIQi3xaq1A==}
|
||||
|
||||
@ -1004,17 +1029,17 @@ packages:
|
||||
engines: {node: '>=18.12.0'}
|
||||
hasBin: true
|
||||
|
||||
'@nuxt/ui@3.3.3':
|
||||
resolution: {integrity: sha512-1JS7V3FqsLQMwt6bzHYackdUtwXU/w4nRoqKLP+5WAXnsXb4nrFInLTh3wnJGsg8N6FKz2qbREimDfNuMfmKUQ==}
|
||||
'@nuxt/ui@4.0.0':
|
||||
resolution: {integrity: sha512-pu5FZ8NZN2YKAiExOXuM0ImjOMe3h4/CsVgm71it+1On7OmIYHeh6SGgvaSX4Ly7FibUFllZMzJ+M5jo6KAEuw==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@inertiajs/vue3': ^2.0.7
|
||||
joi: ^17.13.0
|
||||
joi: ^18.0.0
|
||||
superstruct: ^2.0.0
|
||||
typescript: ^5.6.3
|
||||
valibot: ^1.0.0
|
||||
vue-router: ^4.5.0
|
||||
yup: ^1.6.0
|
||||
yup: ^1.7.0
|
||||
zod: ^3.22.4
|
||||
peerDependenciesMeta:
|
||||
'@inertiajs/vue3':
|
||||
@ -1045,6 +1070,10 @@ packages:
|
||||
resolution: {integrity: sha512-SQqJP6NDlmaoLzs7A74cx0Q3W4Vc+JSBlu3AN0q9+Q07Nvba5osab99GJEQ+PGnjaRwBFh35braUA2hRz9bdSA==}
|
||||
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':
|
||||
resolution: {integrity: sha512-jOgbDgp6A1ax9sxHPRHBxUpxIzp2VTgbZ/6HPKIVUJ7IQqKVsELKFXIOEbCDlb1rUhZZtGf53MFypXf72kR5eQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -2121,6 +2150,11 @@ packages:
|
||||
peerDependencies:
|
||||
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':
|
||||
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
|
||||
cpu: [arm]
|
||||
@ -2446,6 +2480,12 @@ packages:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
|
||||
@ -2837,6 +2877,15 @@ packages:
|
||||
supports-color:
|
||||
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:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
@ -3294,6 +3343,10 @@ packages:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
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:
|
||||
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
||||
engines: {node: '>=16.17'}
|
||||
@ -3383,6 +3436,20 @@ packages:
|
||||
fraction.js@4.3.7:
|
||||
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:
|
||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -3484,6 +3551,9 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
hey-listen@1.0.8:
|
||||
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
|
||||
|
||||
hookable@5.5.3:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
|
||||
@ -3690,6 +3760,9 @@ packages:
|
||||
json-schema-traverse@0.4.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
@ -3944,6 +4017,18 @@ packages:
|
||||
mocked-exports@0.1.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -4767,6 +4852,11 @@ packages:
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
swrv@1.1.0:
|
||||
resolution: {integrity: sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ==}
|
||||
peerDependencies:
|
||||
vue: '>=3.2.26 < 4'
|
||||
|
||||
system-architecture@0.1.0:
|
||||
resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==}
|
||||
engines: {node: '>=18'}
|
||||
@ -4894,6 +4984,9 @@ packages:
|
||||
unhead@2.0.14:
|
||||
resolution: {integrity: sha512-dRP6OCqtShhMVZQe1F4wdt/WsYl2MskxKK+cvfSo0lQnrPJ4oAUQEkxRg7pPP+vJENabhlir31HwAyHUv7wfMg==}
|
||||
|
||||
unhead@2.0.17:
|
||||
resolution: {integrity: sha512-xX3PCtxaE80khRZobyWCVxeFF88/Tg9eJDcJWY9us727nsTC7C449B8BUfVBmiF2+3LjPcmqeoB2iuMs0U4oJQ==}
|
||||
|
||||
unicode-properties@1.4.1:
|
||||
resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==}
|
||||
|
||||
@ -4911,19 +5004,19 @@ packages:
|
||||
unifont@0.4.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-bTuAMMOOqIAyjV4i4UH7P07pO+EsVxmhOzQ2YJ290J6mkLUdozNhb5I/YoOEheeNADC03ent3Qj07X0fWfUpmw==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
unplugin-auto-import@19.3.0:
|
||||
resolution: {integrity: sha512-iIi0u4Gq2uGkAOGqlPJOAMI8vocvjh1clGTfSK4SOrJKrt+tirrixo/FjgBwXQNNdS7ofcr7OxzmOb/RjWxeEQ==}
|
||||
unimport@5.4.0:
|
||||
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'}
|
||||
peerDependencies:
|
||||
'@nuxt/kit': ^3.2.2
|
||||
'@nuxt/kit': ^4.0.0
|
||||
'@vueuse/core': '*'
|
||||
peerDependenciesMeta:
|
||||
'@nuxt/kit':
|
||||
@ -4939,8 +5032,8 @@ packages:
|
||||
resolution: {integrity: sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
unplugin-vue-components@28.8.0:
|
||||
resolution: {integrity: sha512-2Q6ZongpoQzuXDK0ZsVzMoshH0MWZQ1pzVL538G7oIDKRTVzHjppBDS8aB99SADGHN3lpGU7frraCG6yWNoL5Q==}
|
||||
unplugin-vue-components@29.1.0:
|
||||
resolution: {integrity: sha512-z/9ACPXth199s9aCTCdKZAhe5QGOpvzJYP+Hkd0GN1/PpAmsu+W3UlRY3BJAewPqQxh5xi56+Og6mfiCV1Jzpg==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@babel/parser': ^7.15.8
|
||||
@ -5226,8 +5319,8 @@ packages:
|
||||
vue-bundle-renderer@2.1.2:
|
||||
resolution: {integrity: sha512-M4WRBO/O/7G9phGaGH9AOwOnYtY9ZpPoDVpBpRzR2jO5rFL9mgIlQIgums2ljCTC2HL1jDXFQc//CzWcAQHgAw==}
|
||||
|
||||
vue-component-type-helpers@3.0.6:
|
||||
resolution: {integrity: sha512-6CRM8X7EJqWCJOiKPvSLQG+hJPb/Oy2gyJx3pLjUEhY7PuaCthQu3e0zAGI1lqUBobrrk9IT0K8sG2GsCluxoQ==}
|
||||
vue-component-type-helpers@3.0.8:
|
||||
resolution: {integrity: sha512-WyR30Eq15Y/+odrUUMax6FmPbZwAp/HnC7qgR1r3lVFAcqwQ4wUoV79Mbh4SxDy3NiqDa+G4TOKD5xXSgBHo5A==}
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
@ -5388,6 +5481,32 @@ packages:
|
||||
|
||||
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': {}
|
||||
|
||||
'@antfu/install-pkg@1.1.0':
|
||||
@ -6345,7 +6464,7 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- 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:
|
||||
'@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)
|
||||
@ -6366,7 +6485,7 @@ snapshots:
|
||||
ufo: 1.6.1
|
||||
unifont: 0.4.1
|
||||
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:
|
||||
- '@azure/app-configuration'
|
||||
- '@azure/cosmos'
|
||||
@ -6391,28 +6510,6 @@ snapshots:
|
||||
- uploadthing
|
||||
- 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))':
|
||||
dependencies:
|
||||
'@iconify/collections': 1.0.592
|
||||
@ -6516,13 +6613,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@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))
|
||||
'@internationalized/date': 3.9.0
|
||||
'@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/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/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': 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/schema': 4.1.1
|
||||
'@nuxtjs/color-mode': 3.5.2(magicast@0.3.5)
|
||||
@ -6530,7 +6628,7 @@ snapshots:
|
||||
'@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))
|
||||
'@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/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
|
||||
@ -6548,6 +6646,7 @@ snapshots:
|
||||
knitwork: 1.2.0
|
||||
magic-string: 0.30.19
|
||||
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
|
||||
pathe: 2.0.3
|
||||
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
|
||||
typescript: 5.9.2
|
||||
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-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-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: 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))
|
||||
vue-component-type-helpers: 3.0.6
|
||||
vue-component-type-helpers: 3.0.8
|
||||
optionalDependencies:
|
||||
vue-router: 4.5.1(vue@3.5.21(typescript@5.9.2))
|
||||
zod: 3.25.76
|
||||
@ -6575,6 +6674,7 @@ snapshots:
|
||||
- '@babel/parser'
|
||||
- '@capacitor/preferences'
|
||||
- '@deno/kv'
|
||||
- '@emotion/is-prop-valid'
|
||||
- '@netlify/blobs'
|
||||
- '@planetscale/database'
|
||||
- '@upstash/redis'
|
||||
@ -6597,6 +6697,8 @@ snapshots:
|
||||
- magicast
|
||||
- nprogress
|
||||
- qrcode
|
||||
- react
|
||||
- react-dom
|
||||
- sortablejs
|
||||
- supports-color
|
||||
- universal-cookie
|
||||
@ -6670,7 +6772,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@intlify/core': 11.1.12
|
||||
'@intlify/h3': 0.7.1
|
||||
@ -6697,7 +6799,7 @@ snapshots:
|
||||
ufo: 1.6.1
|
||||
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))
|
||||
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-router: 4.5.1(vue@3.5.21(typescript@5.9.2))
|
||||
transitivePeerDependencies:
|
||||
@ -6728,6 +6830,8 @@ snapshots:
|
||||
- uploadthing
|
||||
- vue
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@oxc-minify/binding-android-arm64@0.86.0':
|
||||
optional: true
|
||||
|
||||
@ -7516,6 +7620,12 @@ snapshots:
|
||||
unhead: 2.0.14
|
||||
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':
|
||||
optional: true
|
||||
|
||||
@ -7830,13 +7940,13 @@ snapshots:
|
||||
|
||||
'@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:
|
||||
'@nuxt/kit': 3.19.1(magicast@0.3.5)
|
||||
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
|
||||
'@vueuse/metadata': 13.9.0
|
||||
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)
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
@ -7876,6 +7986,14 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
@ -8257,10 +8375,10 @@ snapshots:
|
||||
|
||||
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:
|
||||
'@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: {}
|
||||
|
||||
@ -8268,6 +8386,10 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
@ -8338,9 +8460,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@libsql/client': 0.15.15
|
||||
'@opentelemetry/api': 1.9.0
|
||||
|
||||
duplexer@0.1.2: {}
|
||||
|
||||
@ -8692,6 +8815,8 @@ snapshots:
|
||||
|
||||
events@3.3.0: {}
|
||||
|
||||
eventsource-parser@3.0.6: {}
|
||||
|
||||
execa@8.0.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
@ -8803,6 +8928,12 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
@ -8906,6 +9037,8 @@ snapshots:
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
hey-listen@1.0.8: {}
|
||||
|
||||
hookable@5.5.3: {}
|
||||
|
||||
http-errors@2.0.0:
|
||||
@ -9080,6 +9213,8 @@ snapshots:
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema@0.4.0: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json5@2.2.3: {}
|
||||
@ -9322,6 +9457,24 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
@ -9338,7 +9491,7 @@ snapshots:
|
||||
|
||||
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:
|
||||
'@cloudflare/kv-asset-handler': 0.4.0
|
||||
'@rollup/plugin-alias': 5.1.1(rollup@4.50.1)
|
||||
@ -9359,7 +9512,7 @@ snapshots:
|
||||
cookie-es: 2.0.0
|
||||
croner: 9.1.0
|
||||
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
|
||||
destr: 2.0.5
|
||||
dot-prop: 9.0.0
|
||||
@ -9405,7 +9558,7 @@ snapshots:
|
||||
unenv: 2.0.0-rc.20
|
||||
unimport: 5.2.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
|
||||
unwasm: 0.3.11
|
||||
youch: 4.1.0-beta.8
|
||||
@ -9493,7 +9646,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@nuxt/cli': 3.28.0(magicast@0.3.5)
|
||||
'@nuxt/devalue': 2.0.2
|
||||
@ -9528,7 +9681,7 @@ snapshots:
|
||||
mlly: 1.8.0
|
||||
mocked-exports: 0.1.1
|
||||
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
|
||||
ofetch: 1.4.1
|
||||
ohash: 2.0.11
|
||||
@ -9552,7 +9705,7 @@ snapshots:
|
||||
unimport: 5.2.0
|
||||
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))
|
||||
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
|
||||
vue: 3.5.21(typescript@5.9.2)
|
||||
vue-bundle-renderer: 2.1.2
|
||||
@ -10392,6 +10545,10 @@ snapshots:
|
||||
picocolors: 1.1.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: {}
|
||||
|
||||
tailwind-merge@3.3.1: {}
|
||||
@ -10513,6 +10670,10 @@ snapshots:
|
||||
dependencies:
|
||||
hookable: 5.5.3
|
||||
|
||||
unhead@2.0.17:
|
||||
dependencies:
|
||||
hookable: 5.5.3
|
||||
|
||||
unicode-properties@1.4.1:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
@ -10532,23 +10693,6 @@ snapshots:
|
||||
css-tree: 3.1.0
|
||||
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:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
@ -10566,14 +10710,31 @@ snapshots:
|
||||
unplugin: 2.3.10
|
||||
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:
|
||||
local-pkg: 1.1.2
|
||||
magic-string: 0.30.19
|
||||
picomatch: 4.0.3
|
||||
unimport: 4.2.0
|
||||
unimport: 5.4.0
|
||||
unplugin: 2.3.10
|
||||
unplugin-utils: 0.2.5
|
||||
unplugin-utils: 0.3.0
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 4.1.1(magicast@0.3.5)
|
||||
'@vueuse/core': 13.9.0(vue@3.5.21(typescript@5.9.2))
|
||||
@ -10588,16 +10749,16 @@ snapshots:
|
||||
pathe: 2.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:
|
||||
chokidar: 3.6.0
|
||||
debug: 4.4.1
|
||||
debug: 4.4.3
|
||||
local-pkg: 1.1.2
|
||||
magic-string: 0.30.19
|
||||
mlly: 1.8.0
|
||||
tinyglobby: 0.2.15
|
||||
unplugin: 2.3.10
|
||||
unplugin-utils: 0.2.5
|
||||
unplugin-utils: 0.3.0
|
||||
vue: 3.5.21(typescript@5.9.2)
|
||||
optionalDependencies:
|
||||
'@babel/parser': 7.28.4
|
||||
@ -10688,7 +10849,7 @@ snapshots:
|
||||
'@unrs/resolver-binding-win32-ia32-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:
|
||||
anymatch: 3.1.3
|
||||
chokidar: 4.0.3
|
||||
@ -10699,7 +10860,7 @@ snapshots:
|
||||
ofetch: 1.4.1
|
||||
ufo: 1.6.1
|
||||
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
|
||||
|
||||
untun@0.1.3:
|
||||
@ -10861,7 +11022,7 @@ snapshots:
|
||||
dependencies:
|
||||
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)):
|
||||
dependencies:
|
||||
|
||||
@ -67,14 +67,21 @@ impl HlcService {
|
||||
|
||||
/// Factory-Funktion: Erstellt und initialisiert einen neuen HLC-Service aus einer bestehenden DB-Verbindung.
|
||||
/// Dies ist die bevorzugte Methode zur Instanziierung.
|
||||
pub fn new_from_connection(
|
||||
conn: &Connection,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<Self, HlcError> {
|
||||
pub fn try_initialize(conn: &Connection, app_handle: &AppHandle) -> Result<Self, HlcError> {
|
||||
// 1. Hole oder erstelle eine persistente Node-ID
|
||||
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))
|
||||
})?;
|
||||
|
||||
@ -112,6 +119,7 @@ impl HlcService {
|
||||
if let Some(s) = value.as_str() {
|
||||
// Das ist unser Erfolgsfall. Wir haben einen &str und können
|
||||
// eine Kopie davon zurückgeben.
|
||||
println!("Gefundene und validierte Geräte-ID: {}", s);
|
||||
if Uuid::parse_str(s).is_ok() {
|
||||
// Erfolgsfall: Der Wert ist ein String UND eine gültige UUID.
|
||||
// Wir können die Funktion direkt mit dem Wert verlassen.
|
||||
|
||||
@ -115,12 +115,8 @@ impl CrdtTransformer {
|
||||
Statement::Query(query) => self.transform_query_recursive(query),
|
||||
// Fange alle anderen Fälle ab und gib einen Fehler zurück
|
||||
_ => Err(DatabaseError::UnsupportedStatement {
|
||||
statement_type: format!("{:?}", stmt)
|
||||
.split('(')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
description: "This operation only accepts SELECT statements.".to_string(),
|
||||
sql: stmt.to_string(),
|
||||
reason: "This operation only accepts SELECT statements.".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@ -168,8 +164,8 @@ impl CrdtTransformer {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(DatabaseError::UnsupportedStatement {
|
||||
statement_type: "DELETE".to_string(),
|
||||
description: "DELETE from non-table source or multiple tables".to_string(),
|
||||
sql: del_stmt.to_string(),
|
||||
reason: "DELETE from non-table source or multiple tables".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -711,15 +707,15 @@ impl CrdtTransformer {
|
||||
}
|
||||
_ => {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
statement_type: "INSERT".to_string(),
|
||||
description: "INSERT with unsupported source type".to_string(),
|
||||
sql: insert_stmt.to_string(),
|
||||
reason: "INSERT with unsupported source type".to_string(),
|
||||
});
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
statement_type: "INSERT".to_string(),
|
||||
description: "INSERT statement has no source".to_string(),
|
||||
reason: "INSERT statement has no source".to_string(),
|
||||
sql: insert_stmt.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -740,8 +736,8 @@ impl CrdtTransformer {
|
||||
from[0].clone()
|
||||
} else {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
statement_type: "DELETE".to_string(),
|
||||
description: "DELETE with multiple tables not supported".to_string(),
|
||||
reason: "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
|
||||
if !matches!(statement, Statement::Query(_)) {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
statement_type: "Non-Query".to_string(),
|
||||
description: "Only SELECT statements are allowed in select function".to_string(),
|
||||
return Err(DatabaseError::StatementError {
|
||||
reason: "Only SELECT statements are allowed in select function".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
// src-tauri/src/database/error.rs
|
||||
|
||||
use crate::crdt::trigger::CrdtSetupError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::crdt::trigger::CrdtSetupError;
|
||||
|
||||
#[derive(Error, Debug, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", content = "details")]
|
||||
@ -13,14 +12,21 @@ pub enum DatabaseError {
|
||||
/// Der SQL-Code konnte nicht geparst werden.
|
||||
#[error("Failed to parse SQL: {reason} - SQL: {sql}")]
|
||||
ParseError { reason: String, sql: String },
|
||||
|
||||
/// Parameter-Fehler (falsche Anzahl, ungültiger Typ, etc.)
|
||||
#[error("Parameter error: {reason} (expected: {expected}, provided: {provided})")]
|
||||
ParamError {
|
||||
reason: String,
|
||||
#[error("Parameter count mismatch: SQL has {expected} placeholders but {provided} provided. SQL Statement: {sql}")]
|
||||
ParameterMismatchError {
|
||||
expected: 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}")]
|
||||
PrepareError { reason: String },
|
||||
|
||||
@ -28,7 +34,7 @@ pub enum DatabaseError {
|
||||
DatabaseError { reason: String },
|
||||
|
||||
/// 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 {
|
||||
sql: String,
|
||||
reason: String,
|
||||
@ -37,34 +43,36 @@ pub enum DatabaseError {
|
||||
/// Ein Fehler ist beim Verwalten der Transaktion aufgetreten.
|
||||
#[error("Transaction error: {reason}")]
|
||||
TransactionError { reason: String },
|
||||
|
||||
/// Ein SQL-Statement wird vom Proxy nicht unterstützt.
|
||||
#[error("Unsupported statement type '{statement_type}': {description}")]
|
||||
UnsupportedStatement {
|
||||
statement_type: String,
|
||||
description: String,
|
||||
},
|
||||
#[error("Unsupported statement. '{reason}'. - SQL: {sql}")]
|
||||
UnsupportedStatement { reason: String, sql: String },
|
||||
|
||||
/// Fehler im HLC-Service
|
||||
#[error("HLC error: {reason}")]
|
||||
HlcError { reason: String },
|
||||
|
||||
/// Fehler beim Sperren der Datenbankverbindung
|
||||
#[error("Lock error: {reason}")]
|
||||
LockError { reason: String },
|
||||
|
||||
/// Fehler bei der Datenbankverbindung
|
||||
#[error("Connection error: {reason}")]
|
||||
ConnectionError { reason: String },
|
||||
|
||||
/// Fehler bei der JSON-Serialisierung
|
||||
#[error("Serialization error: {reason}")]
|
||||
SerializationError { reason: String },
|
||||
|
||||
#[error("Permission error for extension '{extension_id}': {reason} (operation: {}, resource: {})",
|
||||
operation.as_deref().unwrap_or("unknown"),
|
||||
resource.as_deref().unwrap_or("unknown"))]
|
||||
/// Permission-bezogener Fehler für Extensions
|
||||
#[error("Permission error for extension '{extension_id}': {reason} (operation: {operation:?}, resource: {resource:?})")]
|
||||
PermissionError {
|
||||
extension_id: String,
|
||||
operation: Option<String>,
|
||||
resource: Option<String>,
|
||||
reason: String,
|
||||
},
|
||||
|
||||
#[error("Query error: {reason}")]
|
||||
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 {
|
||||
match err {
|
||||
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::database::error::DatabaseError;
|
||||
use crate::table_names::TABLE_CRDT_CONFIGS;
|
||||
use crate::AppState;
|
||||
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]
|
||||
pub fn sql_select(
|
||||
sql: String,
|
||||
@ -166,25 +163,33 @@ pub fn create_encrypted_database(
|
||||
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
|
||||
let mut db = state.db.0.lock().map_err(|e| DatabaseError::LockError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
*db = Some(new_conn);
|
||||
*db = Some(new_conn); */
|
||||
|
||||
Ok(format!("Verschlüsselte CRDT-Datenbank erstellt",))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_encrypted_database(
|
||||
//app_handle: AppHandle,
|
||||
app_handle: AppHandle,
|
||||
path: String,
|
||||
key: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> 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
|
||||
.path()
|
||||
.resolve(format!("vaults/{}", path), BaseDirectory::AppLocalData)
|
||||
@ -196,12 +201,48 @@ pub fn open_encrypted_database(
|
||||
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))?;
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
/// 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 serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tauri::{
|
||||
http::{Request, Response},
|
||||
AppHandle, Error as TauriError, Manager, Runtime, UriSchemeContext,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionManifest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub entry: String,
|
||||
pub icon: Option<String>,
|
||||
pub permissions: ExtensionPermissions,
|
||||
pub homepage: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Extension source type (production vs development)
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExtensionSource {
|
||||
Production {
|
||||
path: PathBuf,
|
||||
version: String,
|
||||
},
|
||||
Development {
|
||||
dev_server_url: String,
|
||||
manifest_path: PathBuf,
|
||||
auto_reload: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Complete extension data structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Extension {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub source: ExtensionSource,
|
||||
pub manifest: ExtensionManifest,
|
||||
pub enabled: bool,
|
||||
pub last_accessed: SystemTime,
|
||||
}
|
||||
|
||||
/// Cached permission data for performance
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedPermission {
|
||||
pub permissions: Vec<DbExtensionPermission>,
|
||||
pub cached_at: SystemTime,
|
||||
pub ttl: Duration,
|
||||
}
|
||||
|
||||
/// Enhanced extension manager
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionManager {
|
||||
pub production_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub dev_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
|
||||
}
|
||||
|
||||
impl ExtensionManager {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Validate filesystem permissions
|
||||
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
|
||||
fs_perms.validate()?;
|
||||
}
|
||||
*/
|
||||
match &extension.source {
|
||||
ExtensionSource::Production { .. } => {
|
||||
let mut extensions = self.production_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Production source".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_dev_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Validate filesystem permissions
|
||||
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
|
||||
fs_perms.validate()?;
|
||||
} */
|
||||
|
||||
match &extension.source {
|
||||
ExtensionSource::Development { .. } => {
|
||||
let mut extensions = self.dev_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Development source".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, extension_id: &str) -> Option<Extension> {
|
||||
// Dev extensions take priority
|
||||
let dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if let Some(extension) = dev_extensions.get(extension_id) {
|
||||
return Some(extension.clone());
|
||||
}
|
||||
|
||||
// Then check production
|
||||
let prod_extensions = self.production_extensions.lock().unwrap();
|
||||
prod_extensions.get(extension_id).cloned()
|
||||
}
|
||||
|
||||
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
|
||||
{
|
||||
let mut dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if dev_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut prod_extensions = self.production_extensions.lock().unwrap();
|
||||
if prod_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ExtensionError::NotFound {
|
||||
id: extension_id.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// For backward compatibility
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionState {
|
||||
pub extensions: Mutex<HashMap<String, ExtensionManifest>>,
|
||||
}
|
||||
|
||||
impl ExtensionState {
|
||||
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
|
||||
let mut extensions = self.extensions.lock().unwrap();
|
||||
extensions.insert(path, manifest);
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
|
||||
let extensions = self.extensions.lock().unwrap();
|
||||
extensions.values().find(|p| p.name == addon_id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ExtensionInfo {
|
||||
id: String,
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
// src-tauri/src/extension/database/mod.rs
|
||||
|
||||
pub mod permissions;
|
||||
|
||||
use crate::crdt::hlc::HlcService;
|
||||
use crate::crdt::transformer::CrdtTransformer;
|
||||
use crate::crdt::trigger;
|
||||
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::database::AppState;
|
||||
use permissions::{check_read_permission, check_write_permission, PermissionError};
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::AppState;
|
||||
use permissions::{check_read_permission, check_write_permission};
|
||||
use rusqlite::params_from_iter;
|
||||
use rusqlite::types::Value as SqlValue;
|
||||
use rusqlite::Transaction;
|
||||
@ -17,36 +17,6 @@ use serde_json::Value as JsonValue;
|
||||
use sqlparser::ast::{Statement, TableFactor, TableObject};
|
||||
use std::collections::HashSet;
|
||||
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
|
||||
pub struct StatementExecutor<'a> {
|
||||
@ -67,30 +37,27 @@ impl<'a> StatementExecutor<'a> {
|
||||
&self,
|
||||
statement: &Statement,
|
||||
params: &[SqlValue],
|
||||
) -> Result<(), ExtensionDatabaseError> {
|
||||
) -> Result<(), DatabaseError> {
|
||||
let sql = statement.to_string();
|
||||
let expected_params = count_sql_placeholders(&sql);
|
||||
|
||||
if expected_params != params.len() {
|
||||
return Err(ExtensionDatabaseError::ParameterValidation {
|
||||
reason: format!(
|
||||
"Parameter count mismatch for statement: {} (expected: {}, provided: {})",
|
||||
truncate_sql(&sql, 100),
|
||||
expected_params,
|
||||
params.len()
|
||||
),
|
||||
return Err(DatabaseError::ParameterMismatchError {
|
||||
expected: expected_params,
|
||||
provided: params.len(),
|
||||
sql,
|
||||
});
|
||||
}
|
||||
|
||||
self.transaction
|
||||
.execute(&sql, params_from_iter(params.iter()))
|
||||
.map_err(|e| ExtensionDatabaseError::StatementExecution {
|
||||
reason: format!(
|
||||
"Failed to execute statement on table {}: {}",
|
||||
.map_err(|e| DatabaseError::ExecutionError {
|
||||
sql,
|
||||
table: Some(
|
||||
self.extract_table_name_from_statement(statement)
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
e
|
||||
),
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@ -147,7 +114,7 @@ pub async fn extension_sql_execute(
|
||||
extension_id: String,
|
||||
state: State<'_, AppState>,
|
||||
hlc_service: State<'_, HlcService>,
|
||||
) -> Result<Vec<String>, ExtensionDatabaseError> {
|
||||
) -> Result<Vec<String>, ExtensionError> {
|
||||
// Permission check
|
||||
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())
|
||||
})
|
||||
.map_err(ExtensionDatabaseError::from)
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -217,7 +184,7 @@ pub async fn extension_sql_select(
|
||||
params: Vec<JsonValue>,
|
||||
extension_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<JsonValue>, ExtensionDatabaseError> {
|
||||
) -> Result<Vec<JsonValue>, ExtensionError> {
|
||||
// Permission check
|
||||
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
|
||||
for stmt in &ast_vec {
|
||||
if !matches!(stmt, Statement::Query(_)) {
|
||||
return Err(ExtensionDatabaseError::StatementExecution {
|
||||
reason: "Only SELECT statements are allowed in extension_sql_select".to_string(),
|
||||
return Err(ExtensionError::Database {
|
||||
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)
|
||||
})
|
||||
.map_err(ExtensionDatabaseError::from)
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
/// Konvertiert eine SQLite-Zeile zu JSON
|
||||
@ -309,16 +281,14 @@ fn row_to_json_value(
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
if total_placeholders != params.len() {
|
||||
return Err(ExtensionDatabaseError::ParameterValidation {
|
||||
reason: format!(
|
||||
"Parameter count mismatch: SQL has {} placeholders but {} parameters provided",
|
||||
total_placeholders,
|
||||
params.len()
|
||||
),
|
||||
return Err(DatabaseError::ParameterMismatchError {
|
||||
expected: total_placeholders,
|
||||
provided: params.len(),
|
||||
sql: sql.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -5,52 +5,18 @@ use crate::database::core::{
|
||||
};
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::database::DbConnection;
|
||||
use crate::models::DbExtensionPermission;
|
||||
use crate::extension::error::ExtensionError;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlparser::ast::{Statement, TableFactor, TableObject};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, Serialize, Deserialize)]
|
||||
pub enum PermissionError {
|
||||
#[error("Extension '{extension_id}' has no {operation} permission for {resource}: {reason}")]
|
||||
AccessDenied {
|
||||
extension_id: String,
|
||||
operation: String,
|
||||
resource: 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(),
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DbExtensionPermission {
|
||||
pub id: String,
|
||||
pub extension_id: String,
|
||||
pub resource: String,
|
||||
pub operation: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Prüft Leseberechtigungen für eine Extension
|
||||
@ -58,9 +24,10 @@ pub async fn check_read_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
sql: &str,
|
||||
) -> Result<(), PermissionError> {
|
||||
let statement = parse_single_statement(sql).map_err(|e| PermissionError::SqlParse {
|
||||
) -> Result<(), ExtensionError> {
|
||||
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
|
||||
reason: e.to_string(),
|
||||
sql: sql.to_string(),
|
||||
})?;
|
||||
|
||||
match statement {
|
||||
@ -68,9 +35,11 @@ pub async fn check_read_permission(
|
||||
let tables = extract_table_names_from_sql(&query.to_string())?;
|
||||
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(),
|
||||
}),
|
||||
sql: sql.to_string(),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,9 +48,10 @@ pub async fn check_write_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
sql: &str,
|
||||
) -> Result<(), PermissionError> {
|
||||
let statement = parse_single_statement(sql).map_err(|e| PermissionError::SqlParse {
|
||||
) -> Result<(), ExtensionError> {
|
||||
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
|
||||
reason: e.to_string(),
|
||||
sql: sql.to_string(),
|
||||
})?;
|
||||
|
||||
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();
|
||||
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
|
||||
fn extract_table_name_from_insert(
|
||||
insert: &sqlparser::ast::Insert,
|
||||
) -> Result<String, PermissionError> {
|
||||
) -> Result<String, ExtensionError> {
|
||||
match &insert.table {
|
||||
TableObject::TableName(name) => Ok(name.to_string()),
|
||||
_ => Err(PermissionError::NoTableSpecified {
|
||||
statement_type: "INSERT".to_string(),
|
||||
}),
|
||||
_ => Err(DatabaseError::NoTableError {
|
||||
sql: insert.to_string(),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrahiert Tabellenname aus TableFactor
|
||||
fn extract_table_name_from_table_factor(
|
||||
table_factor: &TableFactor,
|
||||
) -> Result<String, PermissionError> {
|
||||
) -> Result<String, ExtensionError> {
|
||||
match table_factor {
|
||||
TableFactor::Table { name, .. } => Ok(name.to_string()),
|
||||
_ => Err(PermissionError::InvalidStatement {
|
||||
_ => Err(DatabaseError::StatementError {
|
||||
reason: "Complex table references not supported".to_string(),
|
||||
}),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrahiert Tabellenname aus DELETE-Statement
|
||||
fn extract_table_name_from_delete(
|
||||
delete: &sqlparser::ast::Delete,
|
||||
) -> Result<String, PermissionError> {
|
||||
) -> Result<String, ExtensionError> {
|
||||
use sqlparser::ast::FromTable;
|
||||
|
||||
let table_name = match &delete.from {
|
||||
@ -152,9 +128,10 @@ fn extract_table_name_from_delete(
|
||||
} else if !delete.tables.is_empty() {
|
||||
delete.tables[0].to_string()
|
||||
} else {
|
||||
return Err(PermissionError::NoTableSpecified {
|
||||
statement_type: "DELETE".to_string(),
|
||||
});
|
||||
return Err(DatabaseError::NoTableError {
|
||||
sql: delete.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -168,7 +145,7 @@ async fn check_single_table_permission(
|
||||
extension_id: &str,
|
||||
table_name: &str,
|
||||
operation: &str,
|
||||
) -> Result<(), PermissionError> {
|
||||
) -> Result<(), ExtensionError> {
|
||||
check_table_permissions(
|
||||
connection,
|
||||
extension_id,
|
||||
@ -184,7 +161,7 @@ async fn check_table_permissions(
|
||||
extension_id: &str,
|
||||
table_names: &[String],
|
||||
operation: &str,
|
||||
) -> Result<(), PermissionError> {
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions =
|
||||
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));
|
||||
|
||||
if !has_permission {
|
||||
return Err(PermissionError::access_denied(
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
operation,
|
||||
&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
|
||||
async fn get_extension_permissions(
|
||||
pub async fn get_extension_permissions(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
resource: &str,
|
||||
@ -240,10 +216,7 @@ async fn get_extension_permissions(
|
||||
|
||||
let mut permissions = Vec::new();
|
||||
for row_result in rows {
|
||||
let permission = row_result.map_err(|e| DatabaseError::PermissionError {
|
||||
extension_id: extension_id.to_string(),
|
||||
operation: Some(operation.to_string()),
|
||||
resource: Some(resource.to_string()),
|
||||
let permission = row_result.map_err(|e| DatabaseError::DatabaseError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
permissions.push(permission);
|
||||
@ -255,6 +228,8 @@ async fn get_extension_permissions(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::extension::error::ExtensionError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@ -269,7 +244,7 @@ mod tests {
|
||||
fn test_parse_invalid_sql() {
|
||||
let sql = "INVALID 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());
|
||||
// Wenn du spezifischer sein möchtest, kannst du den DatabaseError-Typ prüfen:
|
||||
match result {
|
||||
@ -284,11 +259,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/* #[test]
|
||||
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 {
|
||||
PermissionError::AccessDenied {
|
||||
ExtensionError::AccessDenied {
|
||||
extension_id,
|
||||
operation,
|
||||
resource,
|
||||
@ -301,5 +276,5 @@ mod tests {
|
||||
}
|
||||
_ => 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 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 database;
|
||||
mod extension;
|
||||
mod models;
|
||||
|
||||
//mod models;
|
||||
|
||||
pub mod table_names {
|
||||
include!(concat!(env!("OUT_DIR"), "/tableNames.rs"));
|
||||
}
|
||||
|
||||
use models::ExtensionState;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
use crate::{crdt::hlc::HlcService, database::DbConnection, extension::core::ExtensionState};
|
||||
|
||||
/* use crate::{
|
||||
crdt::hlc::HlcService,
|
||||
database::{AppState, DbConnection},
|
||||
};
|
||||
extension::core::ExtensionState,
|
||||
}; */
|
||||
|
||||
pub struct AppState {
|
||||
pub db: DbConnection,
|
||||
pub hlc: Mutex<HlcService>, // Kein Arc hier nötig, da der ganze AppState von Tauri in einem Arc verwaltet wird.
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let protocol_name = "haex-extension";
|
||||
|
||||
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) {
|
||||
Ok(response) => response, // Wenn der Handler Ok ist, gib die Response direkt zurück
|
||||
Err(e) => {
|
||||
@ -52,7 +59,7 @@ pub fn run() {
|
||||
})
|
||||
}
|
||||
}
|
||||
}) */
|
||||
})
|
||||
/* .manage(database::DbConnection(Arc::new(Mutex::new(None))))
|
||||
.manage(crdt::hlc::HlcService::new()) */
|
||||
.manage(AppState {
|
||||
|
||||
@ -1,28 +1,45 @@
|
||||
// models.rs
|
||||
// src-tauri/src/models.rs
|
||||
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 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)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionPermissions {
|
||||
pub database: Option<DatabasePermissions>,
|
||||
pub http: Option<Vec<String>>,
|
||||
pub filesystem: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct DatabasePermissions {
|
||||
pub read: Option<Vec<String>>,
|
||||
pub write: Option<Vec<String>>,
|
||||
pub create: Option<Vec<String>>,
|
||||
}
|
||||
/// 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,
|
||||
},
|
||||
} */
|
||||
|
||||
/*
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionState {
|
||||
pub extensions: Mutex<std::collections::HashMap<String, ExtensionManifest>>,
|
||||
@ -48,3 +65,43 @@ pub struct DbExtensionPermission {
|
||||
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 },
|
||||
}
|
||||
*/
|
||||
|
||||
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)
|
||||
console.log('getParentChain1: found group', group, chain)
|
||||
if (group) {
|
||||
/* if (group) {
|
||||
chain.push(group)
|
||||
console.log('getParentChain: found group', group, chain)
|
||||
return getParentChain(group.parentId, chain)
|
||||
}
|
||||
|
||||
return chain.reverse()
|
||||
return chain.reverse() */
|
||||
return []
|
||||
}
|
||||
|
||||
const syncGroupItemsAsync = async () => {
|
||||
@ -322,7 +323,7 @@ const deleteGroupAsync = async (groupId: string, final: boolean = false) => {
|
||||
const items = (await readByGroupIdAsync(groupId)) ?? []
|
||||
console.log('deleteGroupAsync delete Items', items)
|
||||
for (const item of items) {
|
||||
await deleteAsync(item.id, true)
|
||||
if (item) await deleteAsync(item.id, true)
|
||||
}
|
||||
|
||||
return await currentVault?.drizzle
|
||||
|
||||
@ -33,7 +33,6 @@ export const usePasswordItemStore = defineStore('passwordItemStore', () => {
|
||||
|
||||
const syncItemsAsync = async () => {
|
||||
const { currentVault } = useVaultStore()
|
||||
|
||||
items.value =
|
||||
(await currentVault?.drizzle
|
||||
.select()
|
||||
|
||||
Reference in New Issue
Block a user