mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +01:00
Compare commits
11 Commits
0d4059e518
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3897a33565 | |||
| 7487696af4 | |||
| c1ee8e6bc0 | |||
| 2202415441 | |||
| 9583e2f44b | |||
| d886fbd8bd | |||
| 9bad4008f2 | |||
| 203f81e775 | |||
| 554cb7762d | |||
| 5856a73e5b | |||
| 38cc6f36d4 |
26
README.md
26
README.md
@ -168,6 +168,32 @@ pnpm install
|
|||||||
pnpm tauri dev
|
pnpm tauri dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 📦 Release Process
|
||||||
|
|
||||||
|
Create a new release using the automated scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Patch release (0.1.13 → 0.1.14)
|
||||||
|
pnpm release:patch
|
||||||
|
|
||||||
|
# Minor release (0.1.13 → 0.2.0)
|
||||||
|
pnpm release:minor
|
||||||
|
|
||||||
|
# Major release (0.1.13 → 1.0.0)
|
||||||
|
pnpm release:major
|
||||||
|
```
|
||||||
|
|
||||||
|
The script automatically:
|
||||||
|
1. Updates version in `package.json`
|
||||||
|
2. Creates a git commit
|
||||||
|
3. Creates a git tag
|
||||||
|
4. Pushes to remote
|
||||||
|
|
||||||
|
GitHub Actions will then automatically:
|
||||||
|
- Build desktop apps (macOS, Linux, Windows)
|
||||||
|
- Build Android apps (APK and AAB)
|
||||||
|
- Create and publish a GitHub release
|
||||||
|
|
||||||
#### 🧭 Summary
|
#### 🧭 Summary
|
||||||
|
|
||||||
HaexHub aims to:
|
HaexHub aims to:
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "haex-hub",
|
"name": "haex-hub",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.12",
|
"version": "0.1.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
@ -14,10 +14,14 @@
|
|||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
|
"release:patch": "node scripts/release.js patch",
|
||||||
|
"release:minor": "node scripts/release.js minor",
|
||||||
|
"release:major": "node scripts/release.js major",
|
||||||
"tauri:build:debug": "tauri build --debug",
|
"tauri:build:debug": "tauri build --debug",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@haexhub/sdk": "^1.9.10",
|
||||||
"@nuxt/eslint": "1.9.0",
|
"@nuxt/eslint": "1.9.0",
|
||||||
"@nuxt/fonts": "0.11.4",
|
"@nuxt/fonts": "0.11.4",
|
||||||
"@nuxt/icon": "2.0.0",
|
"@nuxt/icon": "2.0.0",
|
||||||
|
|||||||
103
pnpm-lock.yaml
generated
103
pnpm-lock.yaml
generated
@ -11,6 +11,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@haexhub/sdk':
|
||||||
|
specifier: ^1.9.10
|
||||||
|
version: 1.9.10(@libsql/client@0.15.15)(@nuxt/kit@4.2.1(magicast@0.5.1))(@opentelemetry/api@1.9.0)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))
|
||||||
'@nuxt/eslint':
|
'@nuxt/eslint':
|
||||||
specifier: 1.9.0
|
specifier: 1.9.0
|
||||||
version: 1.9.0(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.24)(eslint@9.39.1(jiti@2.6.1))(magicast@0.5.1)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))
|
version: 1.9.0(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.24)(eslint@9.39.1(jiti@2.6.1))(magicast@0.5.1)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))
|
||||||
@ -721,6 +724,27 @@ packages:
|
|||||||
'@floating-ui/vue@1.1.9':
|
'@floating-ui/vue@1.1.9':
|
||||||
resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==}
|
resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==}
|
||||||
|
|
||||||
|
'@haexhub/sdk@1.9.10':
|
||||||
|
resolution: {integrity: sha512-sLzpEGw8v+0+PlO+bNy5lxGShCkesQvGWSV2T8Amj38Gm7eCjnzxCgIzNZ1e8Dp1ICPa+DtXM24E7CMJwJ9Lvg==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@nuxt/kit': ^3.0.0 || ^4.0.0
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
svelte: ^4.0.0 || ^5.0.0
|
||||||
|
vite: ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
vue: ^3.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@nuxt/kit':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
svelte:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
vue:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@humanfs/core@0.19.1':
|
'@humanfs/core@0.19.1':
|
||||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
@ -2420,22 +2444,22 @@ packages:
|
|||||||
'@vue/devtools-api@6.6.4':
|
'@vue/devtools-api@6.6.4':
|
||||||
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
||||||
|
|
||||||
'@vue/devtools-api@7.7.7':
|
'@vue/devtools-api@7.7.8':
|
||||||
resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==}
|
resolution: {integrity: sha512-BtFcAmDbtXGwurWUFf8ogIbgZyR+rcVES1TSNEI8Em80fD8Anu+qTRN1Fc3J6vdRHlVM3fzPV1qIo+B4AiqGzw==}
|
||||||
|
|
||||||
'@vue/devtools-core@8.0.3':
|
'@vue/devtools-core@8.0.3':
|
||||||
resolution: {integrity: sha512-gCEQN7aMmeaigEWJQ2Z2o3g7/CMqGTPvNS1U3n/kzpLoAZ1hkAHNgi4ml/POn/9uqGILBk65GGOUdrraHXRj5Q==}
|
resolution: {integrity: sha512-gCEQN7aMmeaigEWJQ2Z2o3g7/CMqGTPvNS1U3n/kzpLoAZ1hkAHNgi4ml/POn/9uqGILBk65GGOUdrraHXRj5Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.0.0
|
vue: ^3.0.0
|
||||||
|
|
||||||
'@vue/devtools-kit@7.7.7':
|
'@vue/devtools-kit@7.7.8':
|
||||||
resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==}
|
resolution: {integrity: sha512-4Y8op+AoxOJhB9fpcEF6d5vcJXWKgHxC3B0ytUB8zz15KbP9g9WgVzral05xluxi2fOeAy6t140rdQ943GcLRQ==}
|
||||||
|
|
||||||
'@vue/devtools-kit@8.0.3':
|
'@vue/devtools-kit@8.0.3':
|
||||||
resolution: {integrity: sha512-UF4YUOVGdfzXLCv5pMg2DxocB8dvXz278fpgEE+nJ/DRALQGAva7sj9ton0VWZ9hmXw+SV8yKMrxP2MpMhq9Wg==}
|
resolution: {integrity: sha512-UF4YUOVGdfzXLCv5pMg2DxocB8dvXz278fpgEE+nJ/DRALQGAva7sj9ton0VWZ9hmXw+SV8yKMrxP2MpMhq9Wg==}
|
||||||
|
|
||||||
'@vue/devtools-shared@7.7.7':
|
'@vue/devtools-shared@7.7.8':
|
||||||
resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
|
resolution: {integrity: sha512-XHpO3jC5nOgYr40M9p8Z4mmKfTvUxKyRcUnpBAYg11pE78eaRFBKb0kG5yKLroMuJeeNH9LWmKp2zMU5LUc7CA==}
|
||||||
|
|
||||||
'@vue/devtools-shared@8.0.3':
|
'@vue/devtools-shared@8.0.3':
|
||||||
resolution: {integrity: sha512-s/QNll7TlpbADFZrPVsaUNPCOF8NvQgtgmmB7Tip6pLf/HcOvBTly0lfLQ0Eylu9FQ4OqBhFpLyBgwykiSf8zw==}
|
resolution: {integrity: sha512-s/QNll7TlpbADFZrPVsaUNPCOF8NvQgtgmmB7Tip6pLf/HcOvBTly0lfLQ0Eylu9FQ4OqBhFpLyBgwykiSf8zw==}
|
||||||
@ -2714,6 +2738,9 @@ packages:
|
|||||||
birpc@2.7.0:
|
birpc@2.7.0:
|
||||||
resolution: {integrity: sha512-tub/wFGH49vNCm0xraykcY3TcRgX/3JsALYq/Lwrtti+bTyFHkCUAWF5wgYoie8P41wYwig2mIKiqoocr1EkEQ==}
|
resolution: {integrity: sha512-tub/wFGH49vNCm0xraykcY3TcRgX/3JsALYq/Lwrtti+bTyFHkCUAWF5wgYoie8P41wYwig2mIKiqoocr1EkEQ==}
|
||||||
|
|
||||||
|
birpc@2.8.0:
|
||||||
|
resolution: {integrity: sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==}
|
||||||
|
|
||||||
blob-to-buffer@1.2.9:
|
blob-to-buffer@1.2.9:
|
||||||
resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==}
|
resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==}
|
||||||
|
|
||||||
@ -2869,6 +2896,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
|
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
commander@14.0.2:
|
||||||
|
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
commander@2.20.3:
|
commander@2.20.3:
|
||||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
|
|
||||||
@ -6336,6 +6367,48 @@ snapshots:
|
|||||||
- '@vue/composition-api'
|
- '@vue/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@haexhub/sdk@1.9.10(@libsql/client@0.15.15)(@nuxt/kit@4.2.1(magicast@0.5.1))(@opentelemetry/api@1.9.0)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
archiver: 7.0.1
|
||||||
|
commander: 14.0.2
|
||||||
|
drizzle-orm: 0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@nuxt/kit': 4.2.1(magicast@0.5.1)
|
||||||
|
vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||||
|
vue: 3.5.24(typescript@5.9.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@aws-sdk/client-rds-data'
|
||||||
|
- '@cloudflare/workers-types'
|
||||||
|
- '@electric-sql/pglite'
|
||||||
|
- '@libsql/client'
|
||||||
|
- '@libsql/client-wasm'
|
||||||
|
- '@neondatabase/serverless'
|
||||||
|
- '@op-engineering/op-sqlite'
|
||||||
|
- '@opentelemetry/api'
|
||||||
|
- '@planetscale/database'
|
||||||
|
- '@prisma/client'
|
||||||
|
- '@tidbcloud/serverless'
|
||||||
|
- '@types/better-sqlite3'
|
||||||
|
- '@types/pg'
|
||||||
|
- '@types/sql.js'
|
||||||
|
- '@upstash/redis'
|
||||||
|
- '@vercel/postgres'
|
||||||
|
- '@xata.io/client'
|
||||||
|
- bare-abort-controller
|
||||||
|
- better-sqlite3
|
||||||
|
- bun-types
|
||||||
|
- expo-sqlite
|
||||||
|
- gel
|
||||||
|
- knex
|
||||||
|
- kysely
|
||||||
|
- mysql2
|
||||||
|
- pg
|
||||||
|
- postgres
|
||||||
|
- prisma
|
||||||
|
- react-native-b4a
|
||||||
|
- sql.js
|
||||||
|
- sqlite3
|
||||||
|
|
||||||
'@humanfs/core@0.19.1': {}
|
'@humanfs/core@0.19.1': {}
|
||||||
|
|
||||||
'@humanfs/node@0.16.7':
|
'@humanfs/node@0.16.7':
|
||||||
@ -8304,9 +8377,9 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/devtools-api@6.6.4': {}
|
'@vue/devtools-api@6.6.4': {}
|
||||||
|
|
||||||
'@vue/devtools-api@7.7.7':
|
'@vue/devtools-api@7.7.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-kit': 7.7.7
|
'@vue/devtools-kit': 7.7.8
|
||||||
|
|
||||||
'@vue/devtools-core@8.0.3(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))':
|
'@vue/devtools-core@8.0.3(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -8320,10 +8393,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- vite
|
- vite
|
||||||
|
|
||||||
'@vue/devtools-kit@7.7.7':
|
'@vue/devtools-kit@7.7.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-shared': 7.7.7
|
'@vue/devtools-shared': 7.7.8
|
||||||
birpc: 2.7.0
|
birpc: 2.8.0
|
||||||
hookable: 5.5.3
|
hookable: 5.5.3
|
||||||
mitt: 3.0.1
|
mitt: 3.0.1
|
||||||
perfect-debounce: 1.0.0
|
perfect-debounce: 1.0.0
|
||||||
@ -8340,7 +8413,7 @@ snapshots:
|
|||||||
speakingurl: 14.0.1
|
speakingurl: 14.0.1
|
||||||
superjson: 2.2.5
|
superjson: 2.2.5
|
||||||
|
|
||||||
'@vue/devtools-shared@7.7.7':
|
'@vue/devtools-shared@7.7.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
rfdc: 1.4.1
|
rfdc: 1.4.1
|
||||||
|
|
||||||
@ -8618,6 +8691,8 @@ snapshots:
|
|||||||
|
|
||||||
birpc@2.7.0: {}
|
birpc@2.7.0: {}
|
||||||
|
|
||||||
|
birpc@2.8.0: {}
|
||||||
|
|
||||||
blob-to-buffer@1.2.9: {}
|
blob-to-buffer@1.2.9: {}
|
||||||
|
|
||||||
boolbase@1.0.0: {}
|
boolbase@1.0.0: {}
|
||||||
@ -8796,6 +8871,8 @@ snapshots:
|
|||||||
|
|
||||||
commander@11.1.0: {}
|
commander@11.1.0: {}
|
||||||
|
|
||||||
|
commander@14.0.2: {}
|
||||||
|
|
||||||
commander@2.20.3: {}
|
commander@2.20.3: {}
|
||||||
|
|
||||||
commander@7.2.0: {}
|
commander@7.2.0: {}
|
||||||
@ -10733,7 +10810,7 @@ snapshots:
|
|||||||
|
|
||||||
pinia@3.0.3(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)):
|
pinia@3.0.3(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-api': 7.7.7
|
'@vue/devtools-api': 7.7.8
|
||||||
vue: 3.5.24(typescript@5.9.3)
|
vue: 3.5.24(typescript@5.9.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|||||||
91
scripts/release.js
Executable file
91
scripts/release.js
Executable file
@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const rootDir = join(__dirname, '..');
|
||||||
|
|
||||||
|
const versionType = process.argv[2];
|
||||||
|
|
||||||
|
if (!['patch', 'minor', 'major'].includes(versionType)) {
|
||||||
|
console.error('Usage: pnpm release <patch|minor|major>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current package.json
|
||||||
|
const packageJsonPath = join(rootDir, 'package.json');
|
||||||
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
const currentVersion = packageJson.version;
|
||||||
|
|
||||||
|
if (!currentVersion) {
|
||||||
|
console.error('No version found in package.json');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse version
|
||||||
|
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||||
|
|
||||||
|
// Calculate new version
|
||||||
|
let newVersion;
|
||||||
|
switch (versionType) {
|
||||||
|
case 'major':
|
||||||
|
newVersion = `${major + 1}.0.0`;
|
||||||
|
break;
|
||||||
|
case 'minor':
|
||||||
|
newVersion = `${major}.${minor + 1}.0`;
|
||||||
|
break;
|
||||||
|
case 'patch':
|
||||||
|
newVersion = `${major}.${minor}.${patch + 1}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📦 Bumping version from ${currentVersion} to ${newVersion}`);
|
||||||
|
|
||||||
|
// Update package.json
|
||||||
|
packageJson.version = newVersion;
|
||||||
|
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||||
|
console.log('✅ Updated package.json');
|
||||||
|
|
||||||
|
// Git operations
|
||||||
|
try {
|
||||||
|
// Check if there are uncommitted changes
|
||||||
|
const status = execSync('git status --porcelain', { encoding: 'utf8' });
|
||||||
|
const hasOtherChanges = status
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => line && !line.includes('package.json'))
|
||||||
|
.length > 0;
|
||||||
|
|
||||||
|
if (hasOtherChanges) {
|
||||||
|
console.error('❌ There are uncommitted changes besides package.json. Please commit or stash them first.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add and commit package.json
|
||||||
|
execSync('git add package.json', { stdio: 'inherit' });
|
||||||
|
execSync(`git commit -m "Bump version to ${newVersion}"`, { stdio: 'inherit' });
|
||||||
|
console.log('✅ Committed version bump');
|
||||||
|
|
||||||
|
// Create tag
|
||||||
|
execSync(`git tag v${newVersion}`, { stdio: 'inherit' });
|
||||||
|
console.log(`✅ Created tag v${newVersion}`);
|
||||||
|
|
||||||
|
// Push changes and tag
|
||||||
|
console.log('📤 Pushing to remote...');
|
||||||
|
execSync('git push', { stdio: 'inherit' });
|
||||||
|
execSync(`git push origin v${newVersion}`, { stdio: 'inherit' });
|
||||||
|
console.log('✅ Pushed changes and tag');
|
||||||
|
|
||||||
|
console.log('\n🎉 Release v' + newVersion + ' created successfully!');
|
||||||
|
console.log('📋 GitHub Actions will now build and publish the release.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Git operation failed:', error.message);
|
||||||
|
// Rollback package.json changes
|
||||||
|
packageJson.version = currentVersion;
|
||||||
|
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||||
|
console.log('↩️ Rolled back package.json changes');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@ -1,10 +1,10 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { DbAction } from "./DbAction";
|
import type { DbAction } from "./DbAction";
|
||||||
import type { FsAction } from "./FsAction";
|
import type { FsAction } from "./FsAction";
|
||||||
import type { HttpAction } from "./HttpAction";
|
|
||||||
import type { ShellAction } from "./ShellAction";
|
import type { ShellAction } from "./ShellAction";
|
||||||
|
import type { WebAction } from "./WebAction";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ein typsicherer Container, der die spezifische Aktion für einen Ressourcentyp enthält.
|
* Ein typsicherer Container, der die spezifische Aktion für einen Ressourcentyp enthält.
|
||||||
*/
|
*/
|
||||||
export type Action = { "Database": DbAction } | { "Filesystem": FsAction } | { "Http": HttpAction } | { "Shell": ShellAction };
|
export type Action = { "Database": DbAction } | { "Filesystem": FsAction } | { "Web": WebAction } | { "Shell": ShellAction };
|
||||||
|
|||||||
3
src-tauri/bindings/DisplayMode.ts
Normal file
3
src-tauri/bindings/DisplayMode.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type DisplayMode = "auto" | "window" | "iframe";
|
||||||
@ -3,4 +3,4 @@
|
|||||||
/**
|
/**
|
||||||
* Error codes for frontend handling
|
* Error codes for frontend handling
|
||||||
*/
|
*/
|
||||||
export type ExtensionErrorCode = "SecurityViolation" | "NotFound" | "PermissionDenied" | "MutexPoisoned" | "Database" | "Filesystem" | "FilesystemWithPath" | "Http" | "Shell" | "Manifest" | "Validation" | "InvalidPublicKey" | "InvalidSignature" | "InvalidActionString" | "SignatureVerificationFailed" | "CalculateHash" | "Installation";
|
export type ExtensionErrorCode = "SecurityViolation" | "NotFound" | "PermissionDenied" | "MutexPoisoned" | "Database" | "Filesystem" | "FilesystemWithPath" | "Http" | "Web" | "Shell" | "Manifest" | "Validation" | "InvalidPublicKey" | "InvalidSignature" | "InvalidActionString" | "SignatureVerificationFailed" | "CalculateHash" | "Installation";
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { DisplayMode } from "./DisplayMode";
|
||||||
|
|
||||||
export type ExtensionInfoResponse = { id: string, publicKey: string, name: string, version: string, author: string | null, enabled: boolean, description: string | null, homepage: string | null, icon: string | null, entry: string | null, singleInstance: boolean | null, devServerUrl: string | null, };
|
export type ExtensionInfoResponse = { id: string, publicKey: string, name: string, version: string, author: string | null, enabled: boolean, description: string | null, homepage: string | null, icon: string | null, entry: string | null, singleInstance: boolean | null, displayMode: DisplayMode | null, devServerUrl: string | null, };
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { DisplayMode } from "./DisplayMode";
|
||||||
import type { ExtensionPermissions } from "./ExtensionPermissions";
|
import type { ExtensionPermissions } from "./ExtensionPermissions";
|
||||||
|
|
||||||
export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string | null, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, single_instance: boolean | null, };
|
export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string | null, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, single_instance: boolean | null, display_mode: DisplayMode | null, };
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { DbConstraints } from "./DbConstraints";
|
import type { DbConstraints } from "./DbConstraints";
|
||||||
import type { FsConstraints } from "./FsConstraints";
|
import type { FsConstraints } from "./FsConstraints";
|
||||||
import type { HttpConstraints } from "./HttpConstraints";
|
|
||||||
import type { ShellConstraints } from "./ShellConstraints";
|
import type { ShellConstraints } from "./ShellConstraints";
|
||||||
|
import type { WebConstraints } from "./WebConstraints";
|
||||||
|
|
||||||
export type PermissionConstraints = DbConstraints | FsConstraints | HttpConstraints | ShellConstraints;
|
export type PermissionConstraints = DbConstraints | FsConstraints | WebConstraints | ShellConstraints;
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type ResourceType = "fs" | "http" | "db" | "shell";
|
export type ResourceType = "fs" | "web" | "db" | "shell";
|
||||||
|
|||||||
6
src-tauri/bindings/WebAction.ts
Normal file
6
src-tauri/bindings/WebAction.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definiert Aktionen (HTTP-Methoden), die auf Web-Anfragen angewendet werden können.
|
||||||
|
*/
|
||||||
|
export type WebAction = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "*";
|
||||||
4
src-tauri/bindings/WebConstraints.ts
Normal file
4
src-tauri/bindings/WebConstraints.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { RateLimit } from "./RateLimit";
|
||||||
|
|
||||||
|
export type WebConstraints = { methods: Array<string> | null, rate_limit: RateLimit | null, };
|
||||||
@ -1,6 +1,7 @@
|
|||||||
mod generator;
|
mod generator;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
generator::event_names::generate_event_names();
|
||||||
generator::table_names::generate_table_names();
|
generator::table_names::generate_table_names();
|
||||||
generator::rust_types::generate_rust_types();
|
generator::rust_types::generate_rust_types();
|
||||||
tauri_build::build();
|
tauri_build::build();
|
||||||
|
|||||||
16
src-tauri/capabilities/extensions.json
Normal file
16
src-tauri/capabilities/extensions.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "extensions",
|
||||||
|
"description": "Minimal capability for extension webviews - extensions have NO direct system access",
|
||||||
|
"local": true,
|
||||||
|
"webviews": ["ext_*"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"core:webview:default",
|
||||||
|
"notification:default",
|
||||||
|
"notification:allow-is-permission-granted"
|
||||||
|
],
|
||||||
|
"remote": {
|
||||||
|
"urls": ["http://localhost:*", "haex-extension://*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src-tauri/database/migrations/0004_fast_epoch.sql
Normal file
10
src-tauri/database/migrations/0004_fast_epoch.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE `haex_sync_status` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`backend_id` text NOT NULL,
|
||||||
|
`last_pull_sequence` integer,
|
||||||
|
`last_push_hlc_timestamp` text,
|
||||||
|
`last_sync_at` text,
|
||||||
|
`error` text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `haex_extensions` ADD `display_mode` text DEFAULT 'auto';
|
||||||
903
src-tauri/database/migrations/meta/0004_snapshot.json
Normal file
903
src-tauri/database/migrations/meta/0004_snapshot.json
Normal file
@ -0,0 +1,903 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "7ae230a2-4488-4214-9163-602018852676",
|
||||||
|
"prevId": "bf82259e-9264-44e7-a60f-8cc14a1f22e2",
|
||||||
|
"tables": {
|
||||||
|
"haex_crdt_configs": {
|
||||||
|
"name": "haex_crdt_configs",
|
||||||
|
"columns": {
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_crdt_logs": {
|
||||||
|
"name": "haex_crdt_logs",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"table_name": {
|
||||||
|
"name": "table_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"row_pks": {
|
||||||
|
"name": "row_pks",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"op_type": {
|
||||||
|
"name": "op_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"column_name": {
|
||||||
|
"name": "column_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"new_value": {
|
||||||
|
"name": "new_value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"old_value": {
|
||||||
|
"name": "old_value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_haex_timestamp": {
|
||||||
|
"name": "idx_haex_timestamp",
|
||||||
|
"columns": [
|
||||||
|
"haex_timestamp"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_table_row": {
|
||||||
|
"name": "idx_table_row",
|
||||||
|
"columns": [
|
||||||
|
"table_name",
|
||||||
|
"row_pks"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_crdt_snapshots": {
|
||||||
|
"name": "haex_crdt_snapshots",
|
||||||
|
"columns": {
|
||||||
|
"snapshot_id": {
|
||||||
|
"name": "snapshot_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"name": "created",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"epoch_hlc": {
|
||||||
|
"name": "epoch_hlc",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"location_url": {
|
||||||
|
"name": "location_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"file_size_bytes": {
|
||||||
|
"name": "file_size_bytes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_sync_status": {
|
||||||
|
"name": "haex_sync_status",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"backend_id": {
|
||||||
|
"name": "backend_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"last_pull_sequence": {
|
||||||
|
"name": "last_pull_sequence",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"last_push_hlc_timestamp": {
|
||||||
|
"name": "last_push_hlc_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"last_sync_at": {
|
||||||
|
"name": "last_sync_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"name": "error",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_desktop_items": {
|
||||||
|
"name": "haex_desktop_items",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"item_type": {
|
||||||
|
"name": "item_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"extension_id": {
|
||||||
|
"name": "extension_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"system_window_id": {
|
||||||
|
"name": "system_window_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"position_x": {
|
||||||
|
"name": "position_x",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"position_y": {
|
||||||
|
"name": "position_y",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"haex_desktop_items_workspace_id_haex_workspaces_id_fk": {
|
||||||
|
"name": "haex_desktop_items_workspace_id_haex_workspaces_id_fk",
|
||||||
|
"tableFrom": "haex_desktop_items",
|
||||||
|
"tableTo": "haex_workspaces",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"haex_desktop_items_extension_id_haex_extensions_id_fk": {
|
||||||
|
"name": "haex_desktop_items_extension_id_haex_extensions_id_fk",
|
||||||
|
"tableFrom": "haex_desktop_items",
|
||||||
|
"tableTo": "haex_extensions",
|
||||||
|
"columnsFrom": [
|
||||||
|
"extension_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"item_reference": {
|
||||||
|
"name": "item_reference",
|
||||||
|
"value": "(\"haex_desktop_items\".\"item_type\" = 'extension' AND \"haex_desktop_items\".\"extension_id\" IS NOT NULL AND \"haex_desktop_items\".\"system_window_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'system' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'file' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'folder' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"haex_devices": {
|
||||||
|
"name": "haex_devices",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"device_id": {
|
||||||
|
"name": "device_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(CURRENT_TIMESTAMP)"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"haex_devices_device_id_unique": {
|
||||||
|
"name": "haex_devices_device_id_unique",
|
||||||
|
"columns": [
|
||||||
|
"device_id"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_extension_permissions": {
|
||||||
|
"name": "haex_extension_permissions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"extension_id": {
|
||||||
|
"name": "extension_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"resource_type": {
|
||||||
|
"name": "resource_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"name": "action",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"name": "target",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"name": "constraints",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'denied'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(CURRENT_TIMESTAMP)"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"haex_extension_permissions_extension_id_resource_type_action_target_unique": {
|
||||||
|
"name": "haex_extension_permissions_extension_id_resource_type_action_target_unique",
|
||||||
|
"columns": [
|
||||||
|
"extension_id",
|
||||||
|
"resource_type",
|
||||||
|
"action",
|
||||||
|
"target"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"haex_extension_permissions_extension_id_haex_extensions_id_fk": {
|
||||||
|
"name": "haex_extension_permissions_extension_id_haex_extensions_id_fk",
|
||||||
|
"tableFrom": "haex_extension_permissions",
|
||||||
|
"tableTo": "haex_extensions",
|
||||||
|
"columnsFrom": [
|
||||||
|
"extension_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_extensions": {
|
||||||
|
"name": "haex_extensions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"public_key": {
|
||||||
|
"name": "public_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "version",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "author",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"entry": {
|
||||||
|
"name": "entry",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'index.html'"
|
||||||
|
},
|
||||||
|
"homepage": {
|
||||||
|
"name": "homepage",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"name": "icon",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"signature": {
|
||||||
|
"name": "signature",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"single_instance": {
|
||||||
|
"name": "single_instance",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"display_mode": {
|
||||||
|
"name": "display_mode",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'auto'"
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"haex_extensions_public_key_name_unique": {
|
||||||
|
"name": "haex_extensions_public_key_name_unique",
|
||||||
|
"columns": [
|
||||||
|
"public_key",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_notifications": {
|
||||||
|
"name": "haex_notifications",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"alt": {
|
||||||
|
"name": "alt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"name": "date",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"name": "icon",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"read": {
|
||||||
|
"name": "read",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"name": "text",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_settings": {
|
||||||
|
"name": "haex_settings",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"device_id": {
|
||||||
|
"name": "device_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"haex_settings_device_id_key_type_unique": {
|
||||||
|
"name": "haex_settings_device_id_key_type_unique",
|
||||||
|
"columns": [
|
||||||
|
"device_id",
|
||||||
|
"key",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"haex_settings_device_id_haex_devices_id_fk": {
|
||||||
|
"name": "haex_settings_device_id_haex_devices_id_fk",
|
||||||
|
"tableFrom": "haex_settings",
|
||||||
|
"tableTo": "haex_devices",
|
||||||
|
"columnsFrom": [
|
||||||
|
"device_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_sync_backends": {
|
||||||
|
"name": "haex_sync_backends",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"server_url": {
|
||||||
|
"name": "server_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"name": "priority",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(CURRENT_TIMESTAMP)"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_workspaces": {
|
||||||
|
"name": "haex_workspaces",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"device_id": {
|
||||||
|
"name": "device_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"name": "position",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"name": "background",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"haex_workspaces_position_unique": {
|
||||||
|
"name": "haex_workspaces_position_unique",
|
||||||
|
"columns": [
|
||||||
|
"position"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,13 @@
|
|||||||
"when": 1762300795436,
|
"when": 1762300795436,
|
||||||
"tag": "0003_luxuriant_deathstrike",
|
"tag": "0003_luxuriant_deathstrike",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1762894662424,
|
||||||
|
"tag": "0004_fast_epoch",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-webview-show","core:webview:default","core:window:allow-create","core:window:allow-get-all-windows","core:window:allow-show","core:window:default","dialog:default","fs:allow-appconfig-read-recursive","fs:allow-appconfig-write-recursive","fs:allow-appdata-read-recursive","fs:allow-appdata-write-recursive","fs:allow-applocaldata-read-recursive","fs:allow-applocaldata-write-recursive","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-mkdir","fs:allow-exists","fs:allow-remove","fs:allow-resource-read-recursive","fs:allow-resource-write-recursive","fs:allow-download-read-recursive","fs:allow-download-write-recursive","fs:allow-temp-read-recursive","fs:allow-temp-write-recursive","fs:default",{"identifier":"fs:scope","allow":[{"path":"**"},{"path":"$TEMP/**"}]},"http:allow-fetch-send","http:allow-fetch","http:default","notification:allow-create-channel","notification:allow-list-channels","notification:allow-notify","notification:allow-is-permission-granted","notification:default","opener:allow-open-url",{"identifier":"opener:allow-open-path","allow":[{"path":"$TEMP/**"}]},"opener:default","os:allow-hostname","os:default","store:default"]}}
|
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-webview-show","core:webview:default","core:window:allow-create","core:window:allow-get-all-windows","core:window:allow-show","core:window:default","dialog:default","fs:allow-appconfig-read-recursive","fs:allow-appconfig-write-recursive","fs:allow-appdata-read-recursive","fs:allow-appdata-write-recursive","fs:allow-applocaldata-read-recursive","fs:allow-applocaldata-write-recursive","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-mkdir","fs:allow-exists","fs:allow-remove","fs:allow-resource-read-recursive","fs:allow-resource-write-recursive","fs:allow-download-read-recursive","fs:allow-download-write-recursive","fs:allow-temp-read-recursive","fs:allow-temp-write-recursive","fs:default",{"identifier":"fs:scope","allow":[{"path":"**"},{"path":"$TEMP/**"}]},"http:allow-fetch-send","http:allow-fetch","http:default","notification:allow-create-channel","notification:allow-list-channels","notification:allow-notify","notification:allow-is-permission-granted","notification:default","opener:allow-open-url",{"identifier":"opener:allow-open-path","allow":[{"path":"$TEMP/**"}]},"opener:default","os:allow-hostname","os:default","store:default"]},"extensions":{"identifier":"extensions","description":"Minimal capability for extension webviews - extensions have NO direct system access","remote":{"urls":["http://localhost:*","haex-extension://*"]},"local":true,"webviews":["ext_*"],"permissions":["core:default","core:webview:default","notification:default","notification:allow-is-permission-granted"]}}
|
||||||
76
src-tauri/generator/event_names.rs
Normal file
76
src-tauri/generator/event_names.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// src-tauri/generator/event_names.rs
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufReader, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct EventNames {
|
||||||
|
extension: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_event_names() {
|
||||||
|
let out_dir = env::var("OUT_DIR").expect("OUT_DIR ist nicht gesetzt.");
|
||||||
|
println!("Generiere Event-Namen nach {out_dir}");
|
||||||
|
let events_path = Path::new("../src/constants/eventNames.json");
|
||||||
|
let dest_path = Path::new(&out_dir).join("eventNames.rs");
|
||||||
|
|
||||||
|
let file = File::open(events_path).expect("Konnte eventNames.json nicht öffnen");
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let events: EventNames =
|
||||||
|
serde_json::from_reader(reader).expect("Konnte eventNames.json nicht parsen");
|
||||||
|
|
||||||
|
let mut code = String::from(
|
||||||
|
r#"
|
||||||
|
// ==================================================================
|
||||||
|
// HINWEIS: Diese Datei wurde automatisch von build.rs generiert.
|
||||||
|
// Manuelle Änderungen werden bei der nächsten Kompilierung überschrieben!
|
||||||
|
// ==================================================================
|
||||||
|
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extension Events
|
||||||
|
code.push_str("// --- Extension Events ---\n");
|
||||||
|
for (key, value) in &events.extension {
|
||||||
|
let const_name = format!("EVENT_EXTENSION_{}", to_screaming_snake_case(key));
|
||||||
|
code.push_str(&format!(
|
||||||
|
"pub const {}: &str = \"{}\";\n",
|
||||||
|
const_name, value
|
||||||
|
));
|
||||||
|
}
|
||||||
|
code.push('\n');
|
||||||
|
|
||||||
|
// --- Datei schreiben ---
|
||||||
|
let mut f = File::create(&dest_path).expect("Konnte Zieldatei nicht erstellen");
|
||||||
|
f.write_all(code.as_bytes())
|
||||||
|
.expect("Konnte nicht in Zieldatei schreiben");
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-changed=../src/constants/eventNames.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Konvertiert einen String zu SCREAMING_SNAKE_CASE
|
||||||
|
fn to_screaming_snake_case(s: &str) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut prev_is_lower = false;
|
||||||
|
|
||||||
|
for (i, ch) in s.chars().enumerate() {
|
||||||
|
if ch == '_' {
|
||||||
|
result.push('_');
|
||||||
|
prev_is_lower = false;
|
||||||
|
} else if ch.is_uppercase() {
|
||||||
|
if i > 0 && prev_is_lower {
|
||||||
|
result.push('_');
|
||||||
|
}
|
||||||
|
result.push(ch);
|
||||||
|
prev_is_lower = false;
|
||||||
|
} else {
|
||||||
|
result.push(ch.to_ascii_uppercase());
|
||||||
|
prev_is_lower = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
// build/mod.rs
|
// build/mod.rs
|
||||||
|
pub mod event_names;
|
||||||
pub mod rust_types;
|
pub mod rust_types;
|
||||||
pub mod table_names;
|
pub mod table_names;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use crate::database::core::with_connection;
|
|||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
use crate::extension::core::manifest::{EditablePermissions, ExtensionManifest, ExtensionPreview};
|
use crate::extension::core::manifest::{EditablePermissions, ExtensionManifest, ExtensionPreview};
|
||||||
use crate::extension::core::types::{copy_directory, Extension, ExtensionSource};
|
use crate::extension::core::types::{copy_directory, Extension, ExtensionSource};
|
||||||
use crate::extension::core::ExtensionPermissions;
|
use crate::extension::core::{DisplayMode, ExtensionPermissions};
|
||||||
use crate::extension::crypto::ExtensionCrypto;
|
use crate::extension::crypto::ExtensionCrypto;
|
||||||
use crate::extension::database::executor::SqlExecutor;
|
use crate::extension::database::executor::SqlExecutor;
|
||||||
use crate::extension::error::ExtensionError;
|
use crate::extension::error::ExtensionError;
|
||||||
@ -136,12 +136,16 @@ impl ExtensionManager {
|
|||||||
|
|
||||||
// Fallback 1: Check haextension/favicon.ico
|
// Fallback 1: Check haextension/favicon.ico
|
||||||
let haextension_favicon = format!("{haextension_dir}/favicon.ico");
|
let haextension_favicon = format!("{haextension_dir}/favicon.ico");
|
||||||
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, &haextension_favicon, true)? {
|
if let Some(clean_path) =
|
||||||
|
Self::validate_path_in_directory(extension_dir, &haextension_favicon, true)?
|
||||||
|
{
|
||||||
return Ok(Some(clean_path.to_string_lossy().to_string()));
|
return Ok(Some(clean_path.to_string_lossy().to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback 2: Check public/favicon.ico
|
// Fallback 2: Check public/favicon.ico
|
||||||
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, "public/favicon.ico", true)? {
|
if let Some(clean_path) =
|
||||||
|
Self::validate_path_in_directory(extension_dir, "public/favicon.ico", true)?
|
||||||
|
{
|
||||||
return Ok(Some(clean_path.to_string_lossy().to_string()));
|
return Ok(Some(clean_path.to_string_lossy().to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,16 +160,20 @@ impl ExtensionManager {
|
|||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
) -> Result<ExtractedExtension, ExtensionError> {
|
) -> Result<ExtractedExtension, ExtensionError> {
|
||||||
// Use app_cache_dir for better Android compatibility
|
// Use app_cache_dir for better Android compatibility
|
||||||
let cache_dir = app_handle
|
let cache_dir =
|
||||||
.path()
|
app_handle
|
||||||
.app_cache_dir()
|
.path()
|
||||||
.map_err(|e| ExtensionError::InstallationFailed {
|
.app_cache_dir()
|
||||||
reason: format!("Cannot get app cache dir: {e}"),
|
.map_err(|e| ExtensionError::InstallationFailed {
|
||||||
})?;
|
reason: format!("Cannot get app cache dir: {e}"),
|
||||||
|
})?;
|
||||||
|
|
||||||
let temp_id = uuid::Uuid::new_v4();
|
let temp_id = uuid::Uuid::new_v4();
|
||||||
let temp = cache_dir.join(format!("{temp_prefix}_{temp_id}"));
|
let temp = cache_dir.join(format!("{temp_prefix}_{temp_id}"));
|
||||||
let zip_file_path = cache_dir.join(format!("{}_{}_{}.haextension", temp_prefix, temp_id, "temp"));
|
let zip_file_path = cache_dir.join(format!(
|
||||||
|
"{}_{}_{}.haextension",
|
||||||
|
temp_prefix, temp_id, "temp"
|
||||||
|
));
|
||||||
|
|
||||||
// Write bytes to a temporary ZIP file first (important for Android file system)
|
// Write bytes to a temporary ZIP file first (important for Android file system)
|
||||||
fs::write(&zip_file_path, &bytes).map_err(|e| {
|
fs::write(&zip_file_path, &bytes).map_err(|e| {
|
||||||
@ -181,11 +189,10 @@ impl ExtensionManager {
|
|||||||
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
|
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut archive = ZipArchive::new(zip_file).map_err(|e| {
|
let mut archive =
|
||||||
ExtensionError::InstallationFailed {
|
ZipArchive::new(zip_file).map_err(|e| ExtensionError::InstallationFailed {
|
||||||
reason: format!("Invalid ZIP: {e}"),
|
reason: format!("Invalid ZIP: {e}"),
|
||||||
}
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
archive
|
archive
|
||||||
.extract(&temp)
|
.extract(&temp)
|
||||||
@ -199,15 +206,17 @@ impl ExtensionManager {
|
|||||||
// Read haextension_dir from config if it exists, otherwise use default
|
// Read haextension_dir from config if it exists, otherwise use default
|
||||||
let config_path = temp.join("haextension.config.json");
|
let config_path = temp.join("haextension.config.json");
|
||||||
let haextension_dir = if config_path.exists() {
|
let haextension_dir = if config_path.exists() {
|
||||||
let config_content = std::fs::read_to_string(&config_path)
|
let config_content = std::fs::read_to_string(&config_path).map_err(|e| {
|
||||||
.map_err(|e| ExtensionError::ManifestError {
|
ExtensionError::ManifestError {
|
||||||
reason: format!("Cannot read haextension.config.json: {e}"),
|
reason: format!("Cannot read haextension.config.json: {e}"),
|
||||||
})?;
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
let config: serde_json::Value = serde_json::from_str(&config_content)
|
let config: serde_json::Value = serde_json::from_str(&config_content).map_err(|e| {
|
||||||
.map_err(|e| ExtensionError::ManifestError {
|
ExtensionError::ManifestError {
|
||||||
reason: format!("Invalid haextension.config.json: {e}"),
|
reason: format!("Invalid haextension.config.json: {e}"),
|
||||||
})?;
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
let dir = config
|
let dir = config
|
||||||
.get("dev")
|
.get("dev")
|
||||||
@ -237,14 +246,19 @@ impl ExtensionManager {
|
|||||||
let mut manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
let mut manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||||
|
|
||||||
// Validate and resolve icon path with fallback logic
|
// Validate and resolve icon path with fallback logic
|
||||||
let validated_icon = Self::validate_and_resolve_icon_path(&actual_dir, &haextension_dir, manifest.icon.as_deref())?;
|
let validated_icon = Self::validate_and_resolve_icon_path(
|
||||||
|
&actual_dir,
|
||||||
|
&haextension_dir,
|
||||||
|
manifest.icon.as_deref(),
|
||||||
|
)?;
|
||||||
manifest.icon = validated_icon;
|
manifest.icon = validated_icon;
|
||||||
|
|
||||||
let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
|
let content_hash =
|
||||||
ExtensionError::SignatureVerificationFailed {
|
ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
|
||||||
reason: e.to_string(),
|
ExtensionError::SignatureVerificationFailed {
|
||||||
}
|
reason: e.to_string(),
|
||||||
})?;
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(ExtractedExtension {
|
Ok(ExtractedExtension {
|
||||||
temp_dir: actual_dir,
|
temp_dir: actual_dir,
|
||||||
@ -437,9 +451,7 @@ impl ExtensionManager {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
eprintln!("DEBUG: Removing extension with ID: {}", extension.id);
|
eprintln!("DEBUG: Removing extension with ID: {}", extension.id);
|
||||||
eprintln!(
|
eprintln!("DEBUG: Extension name: {extension_name}, version: {extension_version}");
|
||||||
"DEBUG: Extension name: {extension_name}, version: {extension_version}"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Lösche Permissions und Extension-Eintrag in einer Transaktion
|
// Lösche Permissions und Extension-Eintrag in einer Transaktion
|
||||||
with_connection(&state.db, |conn| {
|
with_connection(&state.db, |conn| {
|
||||||
@ -516,7 +528,8 @@ impl ExtensionManager {
|
|||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
file_bytes: Vec<u8>,
|
file_bytes: Vec<u8>,
|
||||||
) -> Result<ExtensionPreview, ExtensionError> {
|
) -> Result<ExtensionPreview, ExtensionError> {
|
||||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview", app_handle)?;
|
let extracted =
|
||||||
|
Self::extract_and_validate_extension(file_bytes, "haexhub_preview", app_handle)?;
|
||||||
|
|
||||||
let is_valid_signature = ExtensionCrypto::verify_signature(
|
let is_valid_signature = ExtensionCrypto::verify_signature(
|
||||||
&extracted.manifest.public_key,
|
&extracted.manifest.public_key,
|
||||||
@ -541,7 +554,8 @@ impl ExtensionManager {
|
|||||||
custom_permissions: EditablePermissions,
|
custom_permissions: EditablePermissions,
|
||||||
state: &State<'_, AppState>,
|
state: &State<'_, AppState>,
|
||||||
) -> Result<String, ExtensionError> {
|
) -> Result<String, ExtensionError> {
|
||||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext", &app_handle)?;
|
let extracted =
|
||||||
|
Self::extract_and_validate_extension(file_bytes, "haexhub_ext", &app_handle)?;
|
||||||
|
|
||||||
// Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft)
|
// Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft)
|
||||||
ExtensionCrypto::verify_signature(
|
ExtensionCrypto::verify_signature(
|
||||||
@ -612,28 +626,29 @@ impl ExtensionManager {
|
|||||||
|
|
||||||
// 1. Extension-Eintrag erstellen mit generierter UUID
|
// 1. Extension-Eintrag erstellen mit generierter UUID
|
||||||
let insert_ext_sql = format!(
|
let insert_ext_sql = format!(
|
||||||
"INSERT INTO {TABLE_EXTENSIONS} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
"INSERT INTO {TABLE_EXTENSIONS} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance, display_mode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||||
);
|
);
|
||||||
|
|
||||||
SqlExecutor::execute_internal_typed(
|
SqlExecutor::execute_internal_typed(
|
||||||
&tx,
|
&tx,
|
||||||
&hlc_service,
|
&hlc_service,
|
||||||
&insert_ext_sql,
|
&insert_ext_sql,
|
||||||
rusqlite::params![
|
rusqlite::params![
|
||||||
extension_id,
|
extension_id,
|
||||||
extracted.manifest.name,
|
extracted.manifest.name,
|
||||||
extracted.manifest.version,
|
extracted.manifest.version,
|
||||||
extracted.manifest.author,
|
extracted.manifest.author,
|
||||||
extracted.manifest.entry,
|
extracted.manifest.entry,
|
||||||
extracted.manifest.icon,
|
extracted.manifest.icon,
|
||||||
extracted.manifest.public_key,
|
extracted.manifest.public_key,
|
||||||
extracted.manifest.signature,
|
extracted.manifest.signature,
|
||||||
extracted.manifest.homepage,
|
extracted.manifest.homepage,
|
||||||
extracted.manifest.description,
|
extracted.manifest.description,
|
||||||
true, // enabled
|
true, // enabled
|
||||||
extracted.manifest.single_instance.unwrap_or(false),
|
extracted.manifest.single_instance.unwrap_or(false),
|
||||||
],
|
extracted.manifest.display_mode.as_ref().map(|dm| format!("{:?}", dm).to_lowercase()).unwrap_or_else(|| "auto".to_string()),
|
||||||
)?;
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
// 2. Permissions speichern
|
// 2. Permissions speichern
|
||||||
let insert_perm_sql = format!(
|
let insert_perm_sql = format!(
|
||||||
@ -709,7 +724,7 @@ impl ExtensionManager {
|
|||||||
// Lade alle Daten aus der Datenbank
|
// Lade alle Daten aus der Datenbank
|
||||||
let extensions = with_connection(&state.db, |conn| {
|
let extensions = with_connection(&state.db, |conn| {
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance FROM {TABLE_EXTENSIONS}"
|
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance, display_mode FROM {TABLE_EXTENSIONS}"
|
||||||
);
|
);
|
||||||
eprintln!("DEBUG: SQL Query before transformation: {sql}");
|
eprintln!("DEBUG: SQL Query before transformation: {sql}");
|
||||||
|
|
||||||
@ -750,6 +765,11 @@ impl ExtensionManager {
|
|||||||
single_instance: row[11]
|
single_instance: row[11]
|
||||||
.as_bool()
|
.as_bool()
|
||||||
.or_else(|| row[11].as_i64().map(|v| v != 0)),
|
.or_else(|| row[11].as_i64().map(|v| v != 0)),
|
||||||
|
display_mode: row[12].as_str().and_then(|s| match s {
|
||||||
|
"window" => Some(DisplayMode::Window),
|
||||||
|
"iframe" => Some(DisplayMode::Iframe),
|
||||||
|
"auto" | _ => Some(DisplayMode::Auto),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let enabled = row[10]
|
let enabled = row[10]
|
||||||
@ -808,14 +828,12 @@ impl ExtensionManager {
|
|||||||
match std::fs::read_to_string(&config_path) {
|
match std::fs::read_to_string(&config_path) {
|
||||||
Ok(config_content) => {
|
Ok(config_content) => {
|
||||||
match serde_json::from_str::<serde_json::Value>(&config_content) {
|
match serde_json::from_str::<serde_json::Value>(&config_content) {
|
||||||
Ok(config) => {
|
Ok(config) => config
|
||||||
config
|
.get("dev")
|
||||||
.get("dev")
|
.and_then(|dev| dev.get("haextension_dir"))
|
||||||
.and_then(|dev| dev.get("haextension_dir"))
|
.and_then(|dir| dir.as_str())
|
||||||
.and_then(|dir| dir.as_str())
|
.unwrap_or("haextension")
|
||||||
.unwrap_or("haextension")
|
.to_string(),
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
Err(_) => "haextension".to_string(),
|
Err(_) => "haextension".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use crate::extension::error::ExtensionError;
|
use crate::extension::error::ExtensionError;
|
||||||
use crate::extension::permissions::types::{
|
use crate::extension::permissions::types::{
|
||||||
Action, DbAction, ExtensionPermission, FsAction, HttpAction, PermissionConstraints,
|
Action, DbAction, ExtensionPermission, FsAction, WebAction, PermissionConstraints,
|
||||||
PermissionStatus, ResourceType, ShellAction,
|
PermissionStatus, ResourceType, ShellAction,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -13,7 +13,8 @@ use ts_rs::TS;
|
|||||||
pub struct PermissionEntry {
|
pub struct PermissionEntry {
|
||||||
pub target: String,
|
pub target: String,
|
||||||
|
|
||||||
/// Die auszuführende Aktion (z.B. "read", "read_write", "GET", "execute").
|
/// Die auszuführende Aktion (z.B. "read", "read_write", "execute").
|
||||||
|
/// Für Web-Permissions ist dies optional und wird ignoriert.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub operation: Option<String>,
|
pub operation: Option<String>,
|
||||||
|
|
||||||
@ -51,10 +52,29 @@ pub struct ExtensionPermissions {
|
|||||||
/// Typ-Alias für bessere Lesbarkeit, wenn die Struktur als UI-Modell verwendet wird.
|
/// Typ-Alias für bessere Lesbarkeit, wenn die Struktur als UI-Modell verwendet wird.
|
||||||
pub type EditablePermissions = ExtensionPermissions;
|
pub type EditablePermissions = ExtensionPermissions;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
|
||||||
|
#[ts(export)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum DisplayMode {
|
||||||
|
/// Platform decides: Desktop = window, Mobile/Web = iframe (default)
|
||||||
|
Auto,
|
||||||
|
/// Always open in native window (if available, falls back to iframe)
|
||||||
|
Window,
|
||||||
|
/// Always open in iframe (embedded in main app)
|
||||||
|
Iframe,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DisplayMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Auto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
|
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct ExtensionManifest {
|
pub struct ExtensionManifest {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(default = "default_version_value")]
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
#[serde(default = "default_entry_value")]
|
#[serde(default = "default_entry_value")]
|
||||||
@ -67,12 +87,18 @@ pub struct ExtensionManifest {
|
|||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub single_instance: Option<bool>,
|
pub single_instance: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub display_mode: Option<DisplayMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_entry_value() -> Option<String> {
|
fn default_entry_value() -> Option<String> {
|
||||||
Some("index.html".to_string())
|
Some("index.html".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_version_value() -> String {
|
||||||
|
"0.0.0-dev".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
impl ExtensionManifest {
|
impl ExtensionManifest {
|
||||||
/// Konvertiert die Manifest-Berechtigungen in das bearbeitbare UI-Modell,
|
/// Konvertiert die Manifest-Berechtigungen in das bearbeitbare UI-Modell,
|
||||||
/// indem der Standardstatus `Granted` gesetzt wird.
|
/// indem der Standardstatus `Granted` gesetzt wird.
|
||||||
@ -117,7 +143,7 @@ impl ExtensionPermissions {
|
|||||||
}
|
}
|
||||||
if let Some(entries) = &self.http {
|
if let Some(entries) = &self.http {
|
||||||
for p in entries {
|
for p in entries {
|
||||||
if let Some(perm) = Self::create_internal(extension_id, ResourceType::Http, p) {
|
if let Some(perm) = Self::create_internal(extension_id, ResourceType::Web, p) {
|
||||||
permissions.push(perm);
|
permissions.push(perm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,7 +172,14 @@ impl ExtensionPermissions {
|
|||||||
ResourceType::Fs => FsAction::from_str(operation_str)
|
ResourceType::Fs => FsAction::from_str(operation_str)
|
||||||
.ok()
|
.ok()
|
||||||
.map(Action::Filesystem),
|
.map(Action::Filesystem),
|
||||||
ResourceType::Http => HttpAction::from_str(operation_str).ok().map(Action::Http),
|
ResourceType::Web => {
|
||||||
|
// For web permissions, operation is optional - default to All
|
||||||
|
if operation_str.is_empty() {
|
||||||
|
Some(Action::Web(WebAction::All))
|
||||||
|
} else {
|
||||||
|
WebAction::from_str(operation_str).ok().map(Action::Web)
|
||||||
|
}
|
||||||
|
}
|
||||||
ResourceType::Shell => ShellAction::from_str(operation_str).ok().map(Action::Shell),
|
ResourceType::Shell => ShellAction::from_str(operation_str).ok().map(Action::Shell),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -181,6 +214,7 @@ pub struct ExtensionInfoResponse {
|
|||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
pub entry: Option<String>,
|
pub entry: Option<String>,
|
||||||
pub single_instance: Option<bool>,
|
pub single_instance: Option<bool>,
|
||||||
|
pub display_mode: Option<DisplayMode>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub dev_server_url: Option<String>,
|
pub dev_server_url: Option<String>,
|
||||||
}
|
}
|
||||||
@ -208,6 +242,7 @@ impl ExtensionInfoResponse {
|
|||||||
icon: extension.manifest.icon.clone(),
|
icon: extension.manifest.icon.clone(),
|
||||||
entry: extension.manifest.entry.clone(),
|
entry: extension.manifest.entry.clone(),
|
||||||
single_instance: extension.manifest.single_instance,
|
single_instance: extension.manifest.single_instance,
|
||||||
|
display_mode: extension.manifest.display_mode.clone(),
|
||||||
dev_server_url,
|
dev_server_url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,10 +25,10 @@ lazy_static::lazy_static! {
|
|||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct ExtensionInfo {
|
pub struct ExtensionInfo {
|
||||||
public_key: String,
|
pub public_key: String,
|
||||||
name: String,
|
pub name: String,
|
||||||
version: String,
|
pub version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@ -141,6 +141,11 @@ pub async fn extension_sql_execute(
|
|||||||
|
|
||||||
let mut statement = ast_vec.pop().unwrap();
|
let mut statement = ast_vec.pop().unwrap();
|
||||||
|
|
||||||
|
// If this is a SELECT statement, delegate to extension_sql_select
|
||||||
|
if matches!(statement, Statement::Query(_)) {
|
||||||
|
return extension_sql_select(sql, params, public_key, name, state).await;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if statement has RETURNING clause
|
// Check if statement has RETURNING clause
|
||||||
let has_returning = crate::database::core::statement_has_returning(&statement);
|
let has_returning = crate::database::core::statement_has_returning(&statement);
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ pub enum ExtensionErrorCode {
|
|||||||
Filesystem = 2001,
|
Filesystem = 2001,
|
||||||
FilesystemWithPath = 2004,
|
FilesystemWithPath = 2004,
|
||||||
Http = 2002,
|
Http = 2002,
|
||||||
|
Web = 2005,
|
||||||
Shell = 2003,
|
Shell = 2003,
|
||||||
Manifest = 3000,
|
Manifest = 3000,
|
||||||
Validation = 3001,
|
Validation = 3001,
|
||||||
@ -83,6 +84,9 @@ pub enum ExtensionError {
|
|||||||
#[error("HTTP request failed: {reason}")]
|
#[error("HTTP request failed: {reason}")]
|
||||||
Http { reason: String },
|
Http { reason: String },
|
||||||
|
|
||||||
|
#[error("Web request failed: {reason}")]
|
||||||
|
WebError { reason: String },
|
||||||
|
|
||||||
#[error("Shell command failed: {reason}")]
|
#[error("Shell command failed: {reason}")]
|
||||||
Shell {
|
Shell {
|
||||||
reason: String,
|
reason: String,
|
||||||
@ -131,6 +135,7 @@ impl ExtensionError {
|
|||||||
ExtensionError::Filesystem { .. } => ExtensionErrorCode::Filesystem,
|
ExtensionError::Filesystem { .. } => ExtensionErrorCode::Filesystem,
|
||||||
ExtensionError::FilesystemWithPath { .. } => ExtensionErrorCode::FilesystemWithPath,
|
ExtensionError::FilesystemWithPath { .. } => ExtensionErrorCode::FilesystemWithPath,
|
||||||
ExtensionError::Http { .. } => ExtensionErrorCode::Http,
|
ExtensionError::Http { .. } => ExtensionErrorCode::Http,
|
||||||
|
ExtensionError::WebError { .. } => ExtensionErrorCode::Web,
|
||||||
ExtensionError::Shell { .. } => ExtensionErrorCode::Shell,
|
ExtensionError::Shell { .. } => ExtensionErrorCode::Shell,
|
||||||
ExtensionError::ManifestError { .. } => ExtensionErrorCode::Manifest,
|
ExtensionError::ManifestError { .. } => ExtensionErrorCode::Manifest,
|
||||||
ExtensionError::ValidationError { .. } => ExtensionErrorCode::Validation,
|
ExtensionError::ValidationError { .. } => ExtensionErrorCode::Validation,
|
||||||
|
|||||||
@ -13,6 +13,10 @@ pub mod database;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod filesystem;
|
pub mod filesystem;
|
||||||
pub mod permissions;
|
pub mod permissions;
|
||||||
|
pub mod web;
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub mod webview;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_extension_info(
|
pub fn get_extension_info(
|
||||||
@ -428,3 +432,85 @@ pub fn get_all_dev_extensions(
|
|||||||
|
|
||||||
Ok(extensions)
|
Ok(extensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WebviewWindow Commands (Desktop only)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_extension_webview_window(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
extension_id: String,
|
||||||
|
title: String,
|
||||||
|
width: f64,
|
||||||
|
height: f64,
|
||||||
|
x: Option<f64>,
|
||||||
|
y: Option<f64>,
|
||||||
|
) -> Result<String, ExtensionError> {
|
||||||
|
eprintln!("[open_extension_webview_window] Received extension_id: {}", extension_id);
|
||||||
|
// Returns the window_id (generated UUID without dashes)
|
||||||
|
state.extension_webview_manager.open_extension_window(
|
||||||
|
&app_handle,
|
||||||
|
&state.extension_manager,
|
||||||
|
extension_id,
|
||||||
|
title,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn close_extension_webview_window(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
window_id: String,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
state
|
||||||
|
.extension_webview_manager
|
||||||
|
.close_extension_window(&app_handle, &window_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn focus_extension_webview_window(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
window_id: String,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
state
|
||||||
|
.extension_webview_manager
|
||||||
|
.focus_extension_window(&app_handle, &window_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn update_extension_webview_window_position(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
window_id: String,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
state
|
||||||
|
.extension_webview_manager
|
||||||
|
.update_extension_window_position(&app_handle, &window_id, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn update_extension_webview_window_size(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
window_id: String,
|
||||||
|
width: f64,
|
||||||
|
height: f64,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
state
|
||||||
|
.extension_webview_manager
|
||||||
|
.update_extension_window_size(&app_handle, &window_id, width, height)
|
||||||
|
}
|
||||||
|
|||||||
65
src-tauri/src/extension/permissions/check.rs
Normal file
65
src-tauri/src/extension/permissions/check.rs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// src-tauri/src/extension/permissions/check.rs
|
||||||
|
|
||||||
|
use crate::extension::error::ExtensionError;
|
||||||
|
use crate::extension::permissions::manager::PermissionManager;
|
||||||
|
use crate::AppState;
|
||||||
|
use std::path::Path;
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_web_permission(
|
||||||
|
extension_id: String,
|
||||||
|
url: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
PermissionManager::check_web_permission(&state, &extension_id, &url).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_database_permission(
|
||||||
|
extension_id: String,
|
||||||
|
resource: String,
|
||||||
|
operation: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
let action = match operation.as_str() {
|
||||||
|
"read" => crate::extension::permissions::types::Action::Database(
|
||||||
|
crate::extension::permissions::types::DbAction::Read,
|
||||||
|
),
|
||||||
|
"write" => crate::extension::permissions::types::Action::Database(
|
||||||
|
crate::extension::permissions::types::DbAction::ReadWrite,
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
return Err(ExtensionError::ValidationError {
|
||||||
|
reason: format!("Invalid database operation: {}", operation),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
PermissionManager::check_database_permission(&state, &extension_id, action, &resource).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_filesystem_permission(
|
||||||
|
extension_id: String,
|
||||||
|
path: String,
|
||||||
|
operation: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
let action = match operation.as_str() {
|
||||||
|
"read" => crate::extension::permissions::types::Action::Filesystem(
|
||||||
|
crate::extension::permissions::types::FsAction::Read,
|
||||||
|
),
|
||||||
|
"write" => crate::extension::permissions::types::Action::Filesystem(
|
||||||
|
crate::extension::permissions::types::FsAction::ReadWrite,
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
return Err(ExtensionError::ValidationError {
|
||||||
|
reason: format!("Invalid filesystem operation: {}", operation),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_path = Path::new(&path);
|
||||||
|
PermissionManager::check_filesystem_permission(&state, &extension_id, action, file_path).await
|
||||||
|
}
|
||||||
@ -2,12 +2,14 @@ use crate::table_names::TABLE_EXTENSION_PERMISSIONS;
|
|||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::database::core::with_connection;
|
use crate::database::core::with_connection;
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
|
use crate::extension::core::types::ExtensionSource;
|
||||||
use crate::extension::database::executor::SqlExecutor;
|
use crate::extension::database::executor::SqlExecutor;
|
||||||
use crate::extension::error::ExtensionError;
|
use crate::extension::error::ExtensionError;
|
||||||
use crate::extension::permissions::types::{Action, ExtensionPermission, PermissionStatus, ResourceType};
|
use crate::extension::permissions::types::{Action, ExtensionPermission, PermissionConstraints, PermissionStatus, ResourceType};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use crate::database::generated::HaexExtensionPermissions;
|
use crate::database::generated::HaexExtensionPermissions;
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
pub struct PermissionManager;
|
pub struct PermissionManager;
|
||||||
|
|
||||||
@ -245,7 +247,97 @@ impl PermissionManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/* /// Prüft Dateisystem-Berechtigungen
|
/// Prüft Web-Berechtigungen für Requests
|
||||||
|
/// Method/operation is not checked - only protocol, domain, port, and path
|
||||||
|
pub async fn check_web_permission(
|
||||||
|
app_state: &State<'_, AppState>,
|
||||||
|
extension_id: &str,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
// Load permissions - for dev extensions, get from manifest; for production, from database
|
||||||
|
let permissions = if let Some(extension) = app_state.extension_manager.get_extension(extension_id) {
|
||||||
|
match &extension.source {
|
||||||
|
ExtensionSource::Development { .. } => {
|
||||||
|
// Dev extension - get web permissions from manifest
|
||||||
|
extension.manifest.permissions
|
||||||
|
.to_internal_permissions(extension_id)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|p| p.resource_type == ResourceType::Web)
|
||||||
|
.map(|mut p| {
|
||||||
|
// Dev extensions have all permissions granted by default
|
||||||
|
p.status = PermissionStatus::Granted;
|
||||||
|
p
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
ExtensionSource::Production { .. } => {
|
||||||
|
// Production extension - load from database
|
||||||
|
with_connection(&app_state.db, |conn| {
|
||||||
|
let sql = format!(
|
||||||
|
"SELECT * FROM {TABLE_EXTENSION_PERMISSIONS} WHERE extension_id = ? AND resource_type = 'web'"
|
||||||
|
);
|
||||||
|
let mut stmt = conn.prepare(&sql).map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
|
let perms_iter = stmt.query_map(params![extension_id], |row| {
|
||||||
|
crate::database::generated::HaexExtensionPermissions::from_row(row)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let permissions: Vec<ExtensionPermission> = perms_iter
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.map(Into::into)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(permissions)
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Extension not found - deny
|
||||||
|
return Err(ExtensionError::ValidationError {
|
||||||
|
reason: format!("Extension not found: {}", extension_id),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let url_parsed = url::Url::parse(url).map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Invalid URL: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let domain = url_parsed.host_str().ok_or_else(|| ExtensionError::ValidationError {
|
||||||
|
reason: "URL does not contain a valid host".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let has_permission = permissions
|
||||||
|
.iter()
|
||||||
|
.filter(|perm| perm.status == PermissionStatus::Granted)
|
||||||
|
.any(|perm| {
|
||||||
|
// Check if target matches the URL
|
||||||
|
let url_matches = if perm.target == "*" {
|
||||||
|
// Wildcard matches everything
|
||||||
|
true
|
||||||
|
} else if perm.target.contains("://") {
|
||||||
|
// URL pattern matching (with protocol and optional path)
|
||||||
|
Self::matches_url_pattern(&perm.target, url)
|
||||||
|
} else {
|
||||||
|
// Domain-only matching (legacy behavior)
|
||||||
|
perm.target == domain || domain.ends_with(&format!(".{}", perm.target))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return the URL match result (no method checking)
|
||||||
|
url_matches
|
||||||
|
});
|
||||||
|
|
||||||
|
if !has_permission {
|
||||||
|
return Err(ExtensionError::permission_denied(
|
||||||
|
extension_id,
|
||||||
|
"web request",
|
||||||
|
url,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prüft Dateisystem-Berechtigungen
|
||||||
pub async fn check_filesystem_permission(
|
pub async fn check_filesystem_permission(
|
||||||
app_state: &State<'_, AppState>,
|
app_state: &State<'_, AppState>,
|
||||||
extension_id: &str,
|
extension_id: &str,
|
||||||
@ -293,56 +385,6 @@ impl PermissionManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prüft HTTP-Berechtigungen
|
|
||||||
pub async fn check_http_permission(
|
|
||||||
app_state: &State<'_, AppState>,
|
|
||||||
extension_id: &str,
|
|
||||||
method: &str,
|
|
||||||
url: &str,
|
|
||||||
) -> Result<(), ExtensionError> {
|
|
||||||
let permissions = Self::get_permissions(app_state, extension_id).await?;
|
|
||||||
|
|
||||||
let url_parsed = Url::parse(url).map_err(|e| ExtensionError::ValidationError {
|
|
||||||
reason: format!("Invalid URL: {}", e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let domain = url_parsed.host_str().unwrap_or("");
|
|
||||||
|
|
||||||
let has_permission = permissions
|
|
||||||
.iter()
|
|
||||||
.filter(|perm| perm.status == PermissionStatus::Granted)
|
|
||||||
.filter(|perm| perm.resource_type == ResourceType::Http)
|
|
||||||
.any(|perm| {
|
|
||||||
let domain_matches = perm.target == "*"
|
|
||||||
|| perm.target == domain
|
|
||||||
|| domain.ends_with(&format!(".{}", perm.target));
|
|
||||||
|
|
||||||
if !domain_matches {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(PermissionConstraints::Http(constraints)) = &perm.constraints {
|
|
||||||
if let Some(methods) = &constraints.methods {
|
|
||||||
if !methods.iter().any(|m| m.eq_ignore_ascii_case(method)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
});
|
|
||||||
|
|
||||||
if !has_permission {
|
|
||||||
return Err(ExtensionError::permission_denied(
|
|
||||||
extension_id,
|
|
||||||
method,
|
|
||||||
&format!("HTTP request to '{}'", url),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prüft Shell-Berechtigungen
|
/// Prüft Shell-Berechtigungen
|
||||||
pub async fn check_shell_permission(
|
pub async fn check_shell_permission(
|
||||||
app_state: &State<'_, AppState>,
|
app_state: &State<'_, AppState>,
|
||||||
@ -405,12 +447,12 @@ impl PermissionManager {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
// Helper-Methoden - müssen DatabaseError statt ExtensionError zurückgeben
|
// Helper-Methoden - müssen DatabaseError statt ExtensionError zurückgeben
|
||||||
pub fn parse_resource_type(s: &str) -> Result<ResourceType, DatabaseError> {
|
pub fn parse_resource_type(s: &str) -> Result<ResourceType, DatabaseError> {
|
||||||
match s {
|
match s {
|
||||||
"fs" => Ok(ResourceType::Fs),
|
"fs" => Ok(ResourceType::Fs),
|
||||||
"http" => Ok(ResourceType::Http),
|
"web" => Ok(ResourceType::Web),
|
||||||
"db" => Ok(ResourceType::Db),
|
"db" => Ok(ResourceType::Db),
|
||||||
"shell" => Ok(ResourceType::Shell),
|
"shell" => Ok(ResourceType::Shell),
|
||||||
_ => Err(DatabaseError::SerializationError {
|
_ => Err(DatabaseError::SerializationError {
|
||||||
@ -441,6 +483,114 @@ impl PermissionManager {
|
|||||||
pattern == path || pattern == "*"
|
pattern == path || pattern == "*"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Matches a URL against a URL pattern
|
||||||
|
/// Supports:
|
||||||
|
/// - Path wildcards: "https://domain.com/*"
|
||||||
|
/// - Subdomain wildcards: "https://*.domain.com/*"
|
||||||
|
fn matches_url_pattern(pattern: &str, url: &str) -> bool {
|
||||||
|
// Parse the actual URL
|
||||||
|
let Ok(url_parsed) = url::Url::parse(url) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if pattern contains subdomain wildcard
|
||||||
|
let has_subdomain_wildcard = pattern.contains("://*.") || pattern.starts_with("*.");
|
||||||
|
|
||||||
|
if has_subdomain_wildcard {
|
||||||
|
// Extract components for wildcard matching
|
||||||
|
// Pattern: "https://*.example.com/*"
|
||||||
|
|
||||||
|
// Get protocol from pattern
|
||||||
|
let protocol_end = pattern.find("://").unwrap_or(0);
|
||||||
|
let pattern_protocol = if protocol_end > 0 {
|
||||||
|
&pattern[..protocol_end]
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
// Protocol must match if specified
|
||||||
|
if !pattern_protocol.is_empty() && pattern_protocol != url_parsed.scheme() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the domain pattern (after *. )
|
||||||
|
let domain_start = if pattern.contains("://*.") {
|
||||||
|
pattern.find("://*.").unwrap() + 5 // length of "://.*"
|
||||||
|
} else if pattern.starts_with("*.") {
|
||||||
|
2 // length of "*."
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find where the domain pattern ends (at / or end of string)
|
||||||
|
let domain_pattern_end = pattern[domain_start..].find('/').map(|i| domain_start + i).unwrap_or(pattern.len());
|
||||||
|
let domain_pattern = &pattern[domain_start..domain_pattern_end];
|
||||||
|
|
||||||
|
// Check if the URL's host ends with the domain pattern
|
||||||
|
let Some(url_host) = url_parsed.host_str() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Match: *.example.com should match subdomain.example.com but not example.com
|
||||||
|
// Also match: exact domain if no subdomain wildcard prefix
|
||||||
|
if !url_host.ends_with(domain_pattern) && url_host != domain_pattern {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For subdomain wildcard, ensure there's actually a subdomain
|
||||||
|
if pattern.contains("*.") && url_host == domain_pattern {
|
||||||
|
return false; // *.example.com should NOT match example.com
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check path wildcard if present
|
||||||
|
if pattern.contains("/*") {
|
||||||
|
// Any path is allowed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check exact path if no wildcard
|
||||||
|
let pattern_path_start = domain_pattern_end;
|
||||||
|
if pattern_path_start < pattern.len() {
|
||||||
|
let pattern_path = &pattern[pattern_path_start..];
|
||||||
|
return url_parsed.path() == pattern_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No subdomain wildcard - parse as full URL
|
||||||
|
let Ok(pattern_url) = url::Url::parse(pattern) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Protocol must match
|
||||||
|
if pattern_url.scheme() != url_parsed.scheme() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host must match
|
||||||
|
if pattern_url.host_str() != url_parsed.host_str() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port must match (if specified)
|
||||||
|
if pattern_url.port() != url_parsed.port() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path matching with wildcard support
|
||||||
|
if pattern.contains("/*") {
|
||||||
|
// Extract the base path before the wildcard
|
||||||
|
if let Some(wildcard_pos) = pattern.find("/*") {
|
||||||
|
let pattern_before_wildcard = &pattern[..wildcard_pos];
|
||||||
|
return url.starts_with(pattern_before_wildcard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact path match (no wildcard)
|
||||||
|
pattern_url.path() == url_parsed.path()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
pub mod check;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod validator;
|
pub mod validator;
|
||||||
|
|||||||
@ -86,11 +86,11 @@ impl FromStr for FsAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Definiert Aktionen (HTTP-Methoden), die auf HTTP-Anfragen angewendet werden können.
|
/// Definiert Aktionen (HTTP-Methoden), die auf Web-Anfragen angewendet werden können.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||||
#[serde(rename_all = "UPPERCASE")]
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub enum HttpAction {
|
pub enum WebAction {
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
@ -100,20 +100,20 @@ pub enum HttpAction {
|
|||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for HttpAction {
|
impl FromStr for WebAction {
|
||||||
type Err = ExtensionError;
|
type Err = ExtensionError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.to_uppercase().as_str() {
|
match s.to_uppercase().as_str() {
|
||||||
"GET" => Ok(HttpAction::Get),
|
"GET" => Ok(WebAction::Get),
|
||||||
"POST" => Ok(HttpAction::Post),
|
"POST" => Ok(WebAction::Post),
|
||||||
"PUT" => Ok(HttpAction::Put),
|
"PUT" => Ok(WebAction::Put),
|
||||||
"PATCH" => Ok(HttpAction::Patch),
|
"PATCH" => Ok(WebAction::Patch),
|
||||||
"DELETE" => Ok(HttpAction::Delete),
|
"DELETE" => Ok(WebAction::Delete),
|
||||||
"*" => Ok(HttpAction::All),
|
"*" => Ok(WebAction::All),
|
||||||
_ => Err(ExtensionError::InvalidActionString {
|
_ => Err(ExtensionError::InvalidActionString {
|
||||||
input: s.to_string(),
|
input: s.to_string(),
|
||||||
resource_type: "http".to_string(),
|
resource_type: "web".to_string(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,7 +149,7 @@ impl FromStr for ShellAction {
|
|||||||
pub enum Action {
|
pub enum Action {
|
||||||
Database(DbAction),
|
Database(DbAction),
|
||||||
Filesystem(FsAction),
|
Filesystem(FsAction),
|
||||||
Http(HttpAction),
|
Web(WebAction),
|
||||||
Shell(ShellAction),
|
Shell(ShellAction),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +173,7 @@ pub struct ExtensionPermission {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub enum ResourceType {
|
pub enum ResourceType {
|
||||||
Fs,
|
Fs,
|
||||||
Http,
|
Web,
|
||||||
Db,
|
Db,
|
||||||
Shell,
|
Shell,
|
||||||
}
|
}
|
||||||
@ -195,7 +195,7 @@ pub enum PermissionStatus {
|
|||||||
pub enum PermissionConstraints {
|
pub enum PermissionConstraints {
|
||||||
Database(DbConstraints),
|
Database(DbConstraints),
|
||||||
Filesystem(FsConstraints),
|
Filesystem(FsConstraints),
|
||||||
Http(HttpConstraints),
|
Web(WebConstraints),
|
||||||
Shell(ShellConstraints),
|
Shell(ShellConstraints),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,7 +223,7 @@ pub struct FsConstraints {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct HttpConstraints {
|
pub struct WebConstraints {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub methods: Option<Vec<String>>,
|
pub methods: Option<Vec<String>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@ -254,7 +254,7 @@ impl ResourceType {
|
|||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
ResourceType::Fs => "fs",
|
ResourceType::Fs => "fs",
|
||||||
ResourceType::Http => "http",
|
ResourceType::Web => "web",
|
||||||
ResourceType::Db => "db",
|
ResourceType::Db => "db",
|
||||||
ResourceType::Shell => "shell",
|
ResourceType::Shell => "shell",
|
||||||
}
|
}
|
||||||
@ -263,7 +263,7 @@ impl ResourceType {
|
|||||||
pub fn from_str(s: &str) -> Result<Self, ExtensionError> {
|
pub fn from_str(s: &str) -> Result<Self, ExtensionError> {
|
||||||
match s {
|
match s {
|
||||||
"fs" => Ok(ResourceType::Fs),
|
"fs" => Ok(ResourceType::Fs),
|
||||||
"http" => Ok(ResourceType::Http),
|
"web" => Ok(ResourceType::Web),
|
||||||
"db" => Ok(ResourceType::Db),
|
"db" => Ok(ResourceType::Db),
|
||||||
"shell" => Ok(ResourceType::Shell),
|
"shell" => Ok(ResourceType::Shell),
|
||||||
_ => Err(ExtensionError::ValidationError {
|
_ => Err(ExtensionError::ValidationError {
|
||||||
@ -284,7 +284,7 @@ impl Action {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.trim_matches('"')
|
.trim_matches('"')
|
||||||
.to_string(),
|
.to_string(),
|
||||||
Action::Http(action) => serde_json::to_string(action)
|
Action::Web(action) => serde_json::to_string(action)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.trim_matches('"')
|
.trim_matches('"')
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@ -299,15 +299,15 @@ impl Action {
|
|||||||
match resource_type {
|
match resource_type {
|
||||||
ResourceType::Db => Ok(Action::Database(DbAction::from_str(s)?)),
|
ResourceType::Db => Ok(Action::Database(DbAction::from_str(s)?)),
|
||||||
ResourceType::Fs => Ok(Action::Filesystem(FsAction::from_str(s)?)),
|
ResourceType::Fs => Ok(Action::Filesystem(FsAction::from_str(s)?)),
|
||||||
ResourceType::Http => {
|
ResourceType::Web => {
|
||||||
let action: HttpAction =
|
let action: WebAction =
|
||||||
serde_json::from_str(&format!("\"{s}\"")).map_err(|_| {
|
serde_json::from_str(&format!("\"{s}\"")).map_err(|_| {
|
||||||
ExtensionError::InvalidActionString {
|
ExtensionError::InvalidActionString {
|
||||||
input: s.to_string(),
|
input: s.to_string(),
|
||||||
resource_type: "http".to_string(),
|
resource_type: "web".to_string(),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
Ok(Action::Http(action))
|
Ok(Action::Web(action))
|
||||||
}
|
}
|
||||||
ResourceType::Shell => Ok(Action::Shell(ShellAction::from_str(s)?)),
|
ResourceType::Shell => Ok(Action::Shell(ShellAction::from_str(s)?)),
|
||||||
}
|
}
|
||||||
|
|||||||
208
src-tauri/src/extension/web/mod.rs
Normal file
208
src-tauri/src/extension/web/mod.rs
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
// src-tauri/src/extension/web/mod.rs
|
||||||
|
|
||||||
|
use crate::extension::error::ExtensionError;
|
||||||
|
use crate::AppState;
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tauri::State;
|
||||||
|
use tauri_plugin_http::reqwest;
|
||||||
|
|
||||||
|
/// Request structure matching the SDK's WebRequestOptions
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct WebFetchRequest {
|
||||||
|
pub url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub method: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub headers: Option<HashMap<String, String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub body: Option<String>, // Base64 encoded
|
||||||
|
#[serde(default)]
|
||||||
|
pub timeout: Option<u64>, // milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response structure matching the SDK's WebResponse
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct WebFetchResponse {
|
||||||
|
pub status: u16,
|
||||||
|
pub status_text: String,
|
||||||
|
pub headers: HashMap<String, String>,
|
||||||
|
pub body: String, // Base64 encoded
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn extension_web_open(
|
||||||
|
url: String,
|
||||||
|
public_key: String,
|
||||||
|
name: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
// Get extension to validate it exists
|
||||||
|
let extension = state
|
||||||
|
.extension_manager
|
||||||
|
.get_extension_by_public_key_and_name(&public_key, &name)?
|
||||||
|
.ok_or_else(|| ExtensionError::NotFound {
|
||||||
|
public_key: public_key.clone(),
|
||||||
|
name: name.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Validate URL format
|
||||||
|
let parsed_url = url::Url::parse(&url).map_err(|e| ExtensionError::WebError {
|
||||||
|
reason: format!("Invalid URL: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Only allow http and https URLs
|
||||||
|
let scheme = parsed_url.scheme();
|
||||||
|
if scheme != "http" && scheme != "https" {
|
||||||
|
return Err(ExtensionError::WebError {
|
||||||
|
reason: format!("Unsupported URL scheme: {}. Only http and https are allowed.", scheme),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check web permissions
|
||||||
|
crate::extension::permissions::manager::PermissionManager::check_web_permission(
|
||||||
|
&state,
|
||||||
|
&extension.id,
|
||||||
|
&url,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Open URL in default browser using tauri-plugin-opener
|
||||||
|
tauri_plugin_opener::open_url(&url, None::<&str>).map_err(|e| ExtensionError::WebError {
|
||||||
|
reason: format!("Failed to open URL in browser: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn extension_web_fetch(
|
||||||
|
url: String,
|
||||||
|
method: Option<String>,
|
||||||
|
headers: Option<HashMap<String, String>>,
|
||||||
|
body: Option<String>,
|
||||||
|
timeout: Option<u64>,
|
||||||
|
public_key: String,
|
||||||
|
name: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<WebFetchResponse, ExtensionError> {
|
||||||
|
// Get extension to validate it exists
|
||||||
|
let extension = state
|
||||||
|
.extension_manager
|
||||||
|
.get_extension_by_public_key_and_name(&public_key, &name)?
|
||||||
|
.ok_or_else(|| ExtensionError::NotFound {
|
||||||
|
public_key: public_key.clone(),
|
||||||
|
name: name.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let method_str = method.as_deref().unwrap_or("GET");
|
||||||
|
|
||||||
|
// Check web permissions before making request
|
||||||
|
crate::extension::permissions::manager::PermissionManager::check_web_permission(
|
||||||
|
&state,
|
||||||
|
&extension.id,
|
||||||
|
&url,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let request = WebFetchRequest {
|
||||||
|
url,
|
||||||
|
method: Some(method_str.to_string()),
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch_web_request(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs the actual HTTP request without CORS restrictions
|
||||||
|
async fn fetch_web_request(request: WebFetchRequest) -> Result<WebFetchResponse, ExtensionError> {
|
||||||
|
let method_str = request.method.as_deref().unwrap_or("GET");
|
||||||
|
let timeout_ms = request.timeout.unwrap_or(30000);
|
||||||
|
|
||||||
|
// Build reqwest client with timeout
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_millis(timeout_ms))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| ExtensionError::WebError {
|
||||||
|
reason: format!("Failed to create HTTP client: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Build request
|
||||||
|
let mut req_builder = match method_str.to_uppercase().as_str() {
|
||||||
|
"GET" => client.get(&request.url),
|
||||||
|
"POST" => client.post(&request.url),
|
||||||
|
"PUT" => client.put(&request.url),
|
||||||
|
"DELETE" => client.delete(&request.url),
|
||||||
|
"PATCH" => client.patch(&request.url),
|
||||||
|
"HEAD" => client.head(&request.url),
|
||||||
|
"OPTIONS" => client.request(reqwest::Method::OPTIONS, &request.url),
|
||||||
|
_ => {
|
||||||
|
return Err(ExtensionError::WebError {
|
||||||
|
reason: format!("Unsupported HTTP method: {}", method_str),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
if let Some(headers) = request.headers {
|
||||||
|
for (key, value) in headers {
|
||||||
|
req_builder = req_builder.header(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add body if present (decode from base64)
|
||||||
|
if let Some(body_base64) = request.body {
|
||||||
|
let body_bytes = STANDARD.decode(&body_base64).map_err(|e| {
|
||||||
|
ExtensionError::WebError {
|
||||||
|
reason: format!("Failed to decode request body from base64: {}", e),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
req_builder = req_builder.body(body_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute request
|
||||||
|
let response = req_builder.send().await.map_err(|e| {
|
||||||
|
if e.is_timeout() {
|
||||||
|
ExtensionError::WebError {
|
||||||
|
reason: format!("Request timeout after {}ms", timeout_ms),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ExtensionError::WebError {
|
||||||
|
reason: format!("Request failed: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Extract response data
|
||||||
|
let status = response.status().as_u16();
|
||||||
|
let status_text = response.status().canonical_reason().unwrap_or("").to_string();
|
||||||
|
let final_url = response.url().to_string();
|
||||||
|
|
||||||
|
// Extract headers
|
||||||
|
let mut response_headers = HashMap::new();
|
||||||
|
for (key, value) in response.headers() {
|
||||||
|
if let Ok(value_str) = value.to_str() {
|
||||||
|
response_headers.insert(key.to_string(), value_str.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read body and encode to base64
|
||||||
|
let body_bytes = response.bytes().await.map_err(|e| ExtensionError::WebError {
|
||||||
|
reason: format!("Failed to read response body: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let body_base64 = STANDARD.encode(&body_bytes);
|
||||||
|
|
||||||
|
Ok(WebFetchResponse {
|
||||||
|
status,
|
||||||
|
status_text,
|
||||||
|
headers: response_headers,
|
||||||
|
body: body_base64,
|
||||||
|
url: final_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
66
src-tauri/src/extension/webview/database.rs
Normal file
66
src-tauri/src/extension/webview/database.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
use crate::extension::database::{extension_sql_execute, extension_sql_select};
|
||||||
|
use crate::extension::error::ExtensionError;
|
||||||
|
use crate::AppState;
|
||||||
|
use tauri::{State, WebviewWindow};
|
||||||
|
|
||||||
|
use super::helpers::get_extension_id;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn webview_extension_db_query(
|
||||||
|
window: WebviewWindow,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
query: String,
|
||||||
|
params: Vec<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, ExtensionError> {
|
||||||
|
let extension_id = get_extension_id(&window, &state)?;
|
||||||
|
|
||||||
|
// Get extension to retrieve public_key and name for existing database functions
|
||||||
|
let extension = state
|
||||||
|
.extension_manager
|
||||||
|
.get_extension(&extension_id)
|
||||||
|
.ok_or_else(|| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Extension with ID {} not found", extension_id),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let rows = extension_sql_select(&query, params, extension.manifest.public_key.clone(), extension.manifest.name.clone(), state)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Database query failed: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"rows": rows,
|
||||||
|
"rowsAffected": 0,
|
||||||
|
"lastInsertId": null
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn webview_extension_db_execute(
|
||||||
|
window: WebviewWindow,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
query: String,
|
||||||
|
params: Vec<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, ExtensionError> {
|
||||||
|
let extension_id = get_extension_id(&window, &state)?;
|
||||||
|
|
||||||
|
// Get extension to retrieve public_key and name for existing database functions
|
||||||
|
let extension = state
|
||||||
|
.extension_manager
|
||||||
|
.get_extension(&extension_id)
|
||||||
|
.ok_or_else(|| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Extension with ID {} not found", extension_id),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let rows = extension_sql_execute(&query, params, extension.manifest.public_key.clone(), extension.manifest.name.clone(), state)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Database execute failed: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"rows": rows,
|
||||||
|
"rowsAffected": rows.len(),
|
||||||
|
"lastInsertId": null
|
||||||
|
}))
|
||||||
|
}
|
||||||
113
src-tauri/src/extension/webview/filesystem.rs
Normal file
113
src-tauri/src/extension/webview/filesystem.rs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
use crate::extension::error::ExtensionError;
|
||||||
|
use crate::AppState;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::{State, WebviewWindow};
|
||||||
|
use tauri_plugin_dialog::DialogExt;
|
||||||
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct FileFilter {
|
||||||
|
pub name: String,
|
||||||
|
pub extensions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct SaveFileResult {
|
||||||
|
pub path: String,
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn webview_extension_fs_save_file(
|
||||||
|
window: WebviewWindow,
|
||||||
|
_state: State<'_, AppState>,
|
||||||
|
data: Vec<u8>,
|
||||||
|
default_path: Option<String>,
|
||||||
|
title: Option<String>,
|
||||||
|
filters: Option<Vec<FileFilter>>,
|
||||||
|
) -> Result<Option<SaveFileResult>, ExtensionError> {
|
||||||
|
eprintln!("[Filesystem] save_file called with {} bytes", data.len());
|
||||||
|
eprintln!("[Filesystem] save_file default_path: {:?}", default_path);
|
||||||
|
eprintln!("[Filesystem] save_file first 10 bytes: {:?}", &data[..data.len().min(10)]);
|
||||||
|
|
||||||
|
// Build save dialog
|
||||||
|
let mut dialog = window.dialog().file();
|
||||||
|
|
||||||
|
if let Some(path) = default_path {
|
||||||
|
dialog = dialog.set_file_name(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(t) = title {
|
||||||
|
dialog = dialog.set_title(&t);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(f) = filters {
|
||||||
|
for filter in f {
|
||||||
|
let ext_refs: Vec<&str> = filter.extensions.iter().map(|s| s.as_str()).collect();
|
||||||
|
dialog = dialog.add_filter(&filter.name, &ext_refs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show dialog (blocking_save_file is safe in async commands)
|
||||||
|
eprintln!("[Filesystem] Showing save dialog...");
|
||||||
|
let file_path = dialog.blocking_save_file();
|
||||||
|
|
||||||
|
if let Some(file_path) = file_path {
|
||||||
|
// Convert FilePath to PathBuf
|
||||||
|
let path_buf = file_path.as_path().ok_or_else(|| ExtensionError::ValidationError {
|
||||||
|
reason: "Failed to get file path".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
eprintln!("[Filesystem] User selected path: {}", path_buf.display());
|
||||||
|
eprintln!("[Filesystem] Writing {} bytes to file...", data.len());
|
||||||
|
|
||||||
|
// Write file using std::fs
|
||||||
|
std::fs::write(path_buf, &data)
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("[Filesystem] ERROR writing file: {}", e);
|
||||||
|
ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to write file: {}", e),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
eprintln!("[Filesystem] File written successfully");
|
||||||
|
|
||||||
|
Ok(Some(SaveFileResult {
|
||||||
|
path: path_buf.to_string_lossy().to_string(),
|
||||||
|
success: true,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
eprintln!("[Filesystem] User cancelled");
|
||||||
|
// User cancelled
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn webview_extension_fs_open_file(
|
||||||
|
window: WebviewWindow,
|
||||||
|
_state: State<'_, AppState>,
|
||||||
|
data: Vec<u8>,
|
||||||
|
file_name: String,
|
||||||
|
) -> Result<serde_json::Value, ExtensionError> {
|
||||||
|
// Get temp directory
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let temp_file_path = temp_dir.join(&file_name);
|
||||||
|
|
||||||
|
// Write file to temp directory using std::fs
|
||||||
|
std::fs::write(&temp_file_path, data)
|
||||||
|
.map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to write temp file: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Open file with system's default viewer
|
||||||
|
let path_str = temp_file_path.to_string_lossy().to_string();
|
||||||
|
window.opener().open_path(path_str, None::<String>)
|
||||||
|
.map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to open file: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"success": true
|
||||||
|
}))
|
||||||
|
}
|
||||||
57
src-tauri/src/extension/webview/helpers.rs
Normal file
57
src-tauri/src/extension/webview/helpers.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
use crate::extension::core::protocol::ExtensionInfo;
|
||||||
|
use crate::extension::error::ExtensionError;
|
||||||
|
use crate::AppState;
|
||||||
|
use tauri::{State, WebviewWindow};
|
||||||
|
|
||||||
|
/// Get extension_id from window (SECURITY: window_id from Tauri, cannot be spoofed)
|
||||||
|
pub fn get_extension_id(window: &WebviewWindow, state: &State<AppState>) -> Result<String, ExtensionError> {
|
||||||
|
let window_id = window.label();
|
||||||
|
eprintln!("[webview_api] Looking up extension_id for window: {}", window_id);
|
||||||
|
|
||||||
|
let windows = state
|
||||||
|
.extension_webview_manager
|
||||||
|
.windows
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| ExtensionError::MutexPoisoned {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
eprintln!("[webview_api] HashMap contents: {:?}", *windows);
|
||||||
|
|
||||||
|
let extension_id = windows
|
||||||
|
.get(window_id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Window {} is not registered as an extension window", window_id),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
eprintln!("[webview_api] Found extension_id: {}", extension_id);
|
||||||
|
Ok(extension_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get full extension info (public_key, name, version) from window
|
||||||
|
pub fn get_extension_info_from_window(
|
||||||
|
window: &WebviewWindow,
|
||||||
|
state: &State<AppState>,
|
||||||
|
) -> Result<ExtensionInfo, ExtensionError> {
|
||||||
|
let extension_id = get_extension_id(window, state)?;
|
||||||
|
|
||||||
|
// Get extension from ExtensionManager using the database UUID
|
||||||
|
let extension = state
|
||||||
|
.extension_manager
|
||||||
|
.get_extension(&extension_id)
|
||||||
|
.ok_or_else(|| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Extension with ID {} not found", extension_id),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let version = match &extension.source {
|
||||||
|
crate::extension::core::types::ExtensionSource::Production { version, .. } => version.clone(),
|
||||||
|
crate::extension::core::types::ExtensionSource::Development { .. } => "dev".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ExtensionInfo {
|
||||||
|
public_key: extension.manifest.public_key,
|
||||||
|
name: extension.manifest.name,
|
||||||
|
version,
|
||||||
|
})
|
||||||
|
}
|
||||||
333
src-tauri/src/extension/webview/manager.rs
Normal file
333
src-tauri/src/extension/webview/manager.rs
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
use crate::event_names::EVENT_EXTENSION_WINDOW_CLOSED;
|
||||||
|
use crate::extension::error::ExtensionError;
|
||||||
|
use crate::extension::ExtensionManager;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||||
|
|
||||||
|
/// Verwaltet native WebviewWindows für Extensions (nur Desktop-Plattformen)
|
||||||
|
pub struct ExtensionWebviewManager {
|
||||||
|
/// Map: window_id -> extension_id
|
||||||
|
/// Das window_id ist ein eindeutiger Identifier (Tauri-kompatibel, keine Bindestriche)
|
||||||
|
/// und wird gleichzeitig als Tauri WebviewWindow label verwendet
|
||||||
|
pub windows: Arc<Mutex<HashMap<String, String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtensionWebviewManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
windows: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Öffnet eine Extension in einem nativen WebviewWindow
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `app_handle` - Tauri AppHandle
|
||||||
|
/// * `extension_manager` - Extension Manager für Zugriff auf Extension-Daten
|
||||||
|
/// * `extension_id` - ID der zu öffnenden Extension
|
||||||
|
/// * `title` - Fenstertitel
|
||||||
|
/// * `width` - Fensterbreite
|
||||||
|
/// * `height` - Fensterhöhe
|
||||||
|
/// * `x` - X-Position (optional)
|
||||||
|
/// * `y` - Y-Position (optional)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Das window_id des erstellten Fensters
|
||||||
|
pub fn open_extension_window(
|
||||||
|
&self,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
extension_manager: &ExtensionManager,
|
||||||
|
extension_id: String,
|
||||||
|
title: String,
|
||||||
|
width: f64,
|
||||||
|
height: f64,
|
||||||
|
x: Option<f64>,
|
||||||
|
y: Option<f64>,
|
||||||
|
) -> Result<String, ExtensionError> {
|
||||||
|
// Extension aus Manager holen
|
||||||
|
let extension = extension_manager
|
||||||
|
.get_extension(&extension_id)
|
||||||
|
.ok_or_else(|| ExtensionError::NotFound {
|
||||||
|
public_key: "".to_string(),
|
||||||
|
name: extension_id.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// URL für Extension generieren (analog zum Frontend)
|
||||||
|
use crate::extension::core::types::ExtensionSource;
|
||||||
|
let url = match &extension.source {
|
||||||
|
ExtensionSource::Production { .. } => {
|
||||||
|
// Für Production Extensions: custom protocol
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
let protocol = "http";
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
let protocol = "haex-extension";
|
||||||
|
|
||||||
|
// Extension Info Base64-codieren (wie im Frontend)
|
||||||
|
let extension_info = serde_json::json!({
|
||||||
|
"publicKey": extension.manifest.public_key,
|
||||||
|
"name": extension.manifest.name,
|
||||||
|
"version": match &extension.source {
|
||||||
|
ExtensionSource::Production { version, .. } => version,
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let extension_info_str = serde_json::to_string(&extension_info)
|
||||||
|
.map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to serialize extension info: {}", e),
|
||||||
|
})?;
|
||||||
|
let extension_info_base64 =
|
||||||
|
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, extension_info_str.as_bytes());
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
let host = "haex-extension.localhost";
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
let host = "localhost";
|
||||||
|
|
||||||
|
let entry = extension.manifest.entry.as_deref().unwrap_or("index.html");
|
||||||
|
format!("{}://{}/{}/{}", protocol, host, extension_info_base64, entry)
|
||||||
|
}
|
||||||
|
ExtensionSource::Development { dev_server_url, .. } => {
|
||||||
|
// Für Dev Extensions: direkt Dev-Server URL
|
||||||
|
dev_server_url.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Eindeutige Window-ID generieren (wird auch als Tauri label verwendet, keine Bindestriche erlaubt)
|
||||||
|
let window_id = format!("ext_{}", uuid::Uuid::new_v4().simple());
|
||||||
|
|
||||||
|
eprintln!("Opening extension window: {} with URL: {}", window_id, url);
|
||||||
|
|
||||||
|
// WebviewWindow erstellen
|
||||||
|
let webview_url = WebviewUrl::External(url.parse().map_err(|e| {
|
||||||
|
ExtensionError::ValidationError {
|
||||||
|
reason: format!("Invalid URL: {}", e),
|
||||||
|
}
|
||||||
|
})?);
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
let mut builder = WebviewWindowBuilder::new(app_handle, &window_id, webview_url)
|
||||||
|
.title(&title)
|
||||||
|
.inner_size(width, height)
|
||||||
|
.decorations(true) // Native Decorations (Titlebar, etc.)
|
||||||
|
.resizable(true)
|
||||||
|
.skip_taskbar(false) // In Taskbar anzeigen
|
||||||
|
.center(); // Fenster zentrieren
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
|
let mut builder = WebviewWindowBuilder::new(app_handle, &window_id, webview_url)
|
||||||
|
.inner_size(width, height);
|
||||||
|
|
||||||
|
// Position setzen, falls angegeben (nur Desktop)
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
if let (Some(x_pos), Some(y_pos)) = (x, y) {
|
||||||
|
builder = builder.position(x_pos, y_pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fenster erstellen
|
||||||
|
let webview_window = builder.build().map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to create webview window: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Event-Listener für das Schließen des Fensters registrieren
|
||||||
|
let window_id_for_event = window_id.clone();
|
||||||
|
let app_handle_for_event = app_handle.clone();
|
||||||
|
let windows_for_event = self.windows.clone();
|
||||||
|
|
||||||
|
webview_window.on_window_event(move |event| {
|
||||||
|
if let tauri::WindowEvent::Destroyed = event {
|
||||||
|
eprintln!("WebviewWindow destroyed: {}", window_id_for_event);
|
||||||
|
|
||||||
|
// Registry cleanup
|
||||||
|
if let Ok(mut windows) = windows_for_event.lock() {
|
||||||
|
windows.remove(&window_id_for_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit event an Frontend, damit das Tracking aktualisiert wird
|
||||||
|
let _ = app_handle_for_event.emit(EVENT_EXTENSION_WINDOW_CLOSED, &window_id_for_event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// In Registry speichern
|
||||||
|
let mut windows = self.windows.lock().map_err(|e| ExtensionError::MutexPoisoned {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
windows.insert(window_id.clone(), extension_id.clone());
|
||||||
|
|
||||||
|
eprintln!("Extension window opened successfully: {}", window_id);
|
||||||
|
Ok(window_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schließt ein Extension-Fenster
|
||||||
|
pub fn close_extension_window(
|
||||||
|
&self,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
window_id: &str,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
let mut windows = self.windows.lock().map_err(|e| ExtensionError::MutexPoisoned {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if windows.remove(window_id).is_some() {
|
||||||
|
drop(windows); // Release lock before potentially blocking operation
|
||||||
|
|
||||||
|
// Webview Window schließen (nur Desktop)
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
if let Some(window) = app_handle.get_webview_window(window_id) {
|
||||||
|
window.close().map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to close window: {}", e),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
eprintln!("Extension window closed: {}", window_id);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ExtensionError::NotFound {
|
||||||
|
public_key: "".to_string(),
|
||||||
|
name: window_id.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fokussiert ein Extension-Fenster
|
||||||
|
pub fn focus_extension_window(
|
||||||
|
&self,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
window_id: &str,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
let windows = self.windows.lock().map_err(|e| ExtensionError::MutexPoisoned {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let exists = windows.contains_key(window_id);
|
||||||
|
drop(windows); // Release lock
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
if let Some(window) = app_handle.get_webview_window(window_id) {
|
||||||
|
window.set_focus().map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to focus window: {}", e),
|
||||||
|
})?;
|
||||||
|
// Zusätzlich nach vorne bringen
|
||||||
|
window.set_always_on_top(true).ok();
|
||||||
|
window.set_always_on_top(false).ok();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ExtensionError::NotFound {
|
||||||
|
public_key: "".to_string(),
|
||||||
|
name: window_id.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aktualisiert Position eines Extension-Fensters
|
||||||
|
pub fn update_extension_window_position(
|
||||||
|
&self,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
window_id: &str,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
let windows = self.windows.lock().map_err(|e| ExtensionError::MutexPoisoned {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let exists = windows.contains_key(window_id);
|
||||||
|
drop(windows); // Release lock
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
if let Some(window) = app_handle.get_webview_window(window_id) {
|
||||||
|
use tauri::Position;
|
||||||
|
window
|
||||||
|
.set_position(Position::Physical(tauri::PhysicalPosition {
|
||||||
|
x: x as i32,
|
||||||
|
y: y as i32,
|
||||||
|
}))
|
||||||
|
.map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to set window position: {}", e),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ExtensionError::NotFound {
|
||||||
|
public_key: "".to_string(),
|
||||||
|
name: window_id.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aktualisiert Größe eines Extension-Fensters
|
||||||
|
pub fn update_extension_window_size(
|
||||||
|
&self,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
window_id: &str,
|
||||||
|
width: f64,
|
||||||
|
height: f64,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
let windows = self.windows.lock().map_err(|e| ExtensionError::MutexPoisoned {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let exists = windows.contains_key(window_id);
|
||||||
|
drop(windows); // Release lock
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
if let Some(window) = app_handle.get_webview_window(window_id) {
|
||||||
|
use tauri::Size;
|
||||||
|
window
|
||||||
|
.set_size(Size::Physical(tauri::PhysicalSize {
|
||||||
|
width: width as u32,
|
||||||
|
height: height as u32,
|
||||||
|
}))
|
||||||
|
.map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to set window size: {}", e),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ExtensionError::NotFound {
|
||||||
|
public_key: "".to_string(),
|
||||||
|
name: window_id.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits an event to all extension webview windows
|
||||||
|
pub fn emit_to_all_extensions<S: serde::Serialize + Clone>(
|
||||||
|
&self,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
event: &str,
|
||||||
|
payload: S,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
let windows = self.windows.lock().map_err(|e| ExtensionError::MutexPoisoned {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
eprintln!("[Manager] Emitting event '{}' to {} webview windows", event, windows.len());
|
||||||
|
|
||||||
|
// Iterate over all window IDs
|
||||||
|
for window_id in windows.keys() {
|
||||||
|
eprintln!("[Manager] Trying to emit to window: {}", window_id);
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
if let Some(window) = app_handle.get_webview_window(window_id) {
|
||||||
|
// Emit event to this specific webview window
|
||||||
|
match window.emit(event, payload.clone()) {
|
||||||
|
Ok(_) => eprintln!("[Manager] Successfully emitted event '{}' to window {}", event, window_id),
|
||||||
|
Err(e) => eprintln!("[Manager] Failed to emit event {} to window {}: {}", event, window_id, e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("[Manager] Window not found: {}", window_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ExtensionWebviewManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src-tauri/src/extension/webview/mod.rs
Normal file
8
src-tauri/src/extension/webview/mod.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
pub mod database;
|
||||||
|
pub mod filesystem;
|
||||||
|
pub mod helpers;
|
||||||
|
pub mod manager;
|
||||||
|
pub mod web;
|
||||||
|
|
||||||
|
// Re-export manager types
|
||||||
|
pub use manager::ExtensionWebviewManager;
|
||||||
266
src-tauri/src/extension/webview/web.rs
Normal file
266
src-tauri/src/extension/webview/web.rs
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
use crate::extension::core::protocol::ExtensionInfo;
|
||||||
|
use crate::extension::error::ExtensionError;
|
||||||
|
use crate::extension::permissions::manager::PermissionManager;
|
||||||
|
use crate::extension::permissions::types::{Action, DbAction, FsAction};
|
||||||
|
use crate::AppState;
|
||||||
|
use base64::Engine;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::{State, WebviewWindow};
|
||||||
|
use tauri_plugin_http::reqwest;
|
||||||
|
|
||||||
|
use super::helpers::{get_extension_id, get_extension_info_from_window};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types for SDK communication
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ApplicationContext {
|
||||||
|
pub theme: String,
|
||||||
|
pub locale: String,
|
||||||
|
pub platform: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Extension Info Command
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn webview_extension_get_info(
|
||||||
|
window: WebviewWindow,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<ExtensionInfo, ExtensionError> {
|
||||||
|
get_extension_info_from_window(&window, &state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Context API Commands
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn webview_extension_context_get(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<ApplicationContext, ExtensionError> {
|
||||||
|
let context = state.context.lock().map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to lock context: {}", e),
|
||||||
|
})?;
|
||||||
|
Ok(context.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn webview_extension_context_set(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
context: ApplicationContext,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
let mut current_context = state.context.lock().map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to lock context: {}", e),
|
||||||
|
})?;
|
||||||
|
*current_context = context;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Permission API Commands
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn webview_extension_check_web_permission(
|
||||||
|
window: WebviewWindow,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
url: String,
|
||||||
|
) -> Result<bool, ExtensionError> {
|
||||||
|
let extension_id = get_extension_id(&window, &state)?;
|
||||||
|
|
||||||
|
match PermissionManager::check_web_permission(&state, &extension_id, &url).await {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn webview_extension_check_database_permission(
|
||||||
|
window: WebviewWindow,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
resource: String,
|
||||||
|
operation: String,
|
||||||
|
) -> Result<bool, ExtensionError> {
|
||||||
|
let extension_id = get_extension_id(&window, &state)?;
|
||||||
|
|
||||||
|
let action = match operation.as_str() {
|
||||||
|
"read" => Action::Database(DbAction::Read),
|
||||||
|
"write" => Action::Database(DbAction::ReadWrite),
|
||||||
|
_ => return Ok(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
match PermissionManager::check_database_permission(&state, &extension_id, action, &resource).await {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn webview_extension_check_filesystem_permission(
|
||||||
|
window: WebviewWindow,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
path: String,
|
||||||
|
action_str: String,
|
||||||
|
) -> Result<bool, ExtensionError> {
|
||||||
|
let extension_id = get_extension_id(&window, &state)?;
|
||||||
|
|
||||||
|
let action = match action_str.as_str() {
|
||||||
|
"read" => Action::Filesystem(FsAction::Read),
|
||||||
|
"write" => Action::Filesystem(FsAction::ReadWrite),
|
||||||
|
_ => return Ok(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let path_buf = std::path::Path::new(&path);
|
||||||
|
match PermissionManager::check_filesystem_permission(&state, &extension_id, action, path_buf).await {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Web API Commands
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn webview_extension_web_open(
|
||||||
|
window: WebviewWindow,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
url: String,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
let extension_id = get_extension_id(&window, &state)?;
|
||||||
|
|
||||||
|
// Validate URL format
|
||||||
|
let parsed_url = url::Url::parse(&url).map_err(|e| ExtensionError::WebError {
|
||||||
|
reason: format!("Invalid URL: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Only allow http and https URLs
|
||||||
|
let scheme = parsed_url.scheme();
|
||||||
|
if scheme != "http" && scheme != "https" {
|
||||||
|
return Err(ExtensionError::WebError {
|
||||||
|
reason: format!("Unsupported URL scheme: {}. Only http and https are allowed.", scheme),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check web permissions
|
||||||
|
PermissionManager::check_web_permission(&state, &extension_id, &url).await?;
|
||||||
|
|
||||||
|
// Open URL in default browser using tauri-plugin-opener
|
||||||
|
tauri_plugin_opener::open_url(&url, None::<&str>).map_err(|e| ExtensionError::WebError {
|
||||||
|
reason: format!("Failed to open URL in browser: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn webview_extension_web_request(
|
||||||
|
window: WebviewWindow,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
url: String,
|
||||||
|
method: Option<String>,
|
||||||
|
headers: Option<serde_json::Value>,
|
||||||
|
body: Option<String>,
|
||||||
|
) -> Result<serde_json::Value, ExtensionError> {
|
||||||
|
let extension_id = get_extension_id(&window, &state)?;
|
||||||
|
|
||||||
|
// Check permission first
|
||||||
|
PermissionManager::check_web_permission(&state, &extension_id, &url).await?;
|
||||||
|
|
||||||
|
// Build request
|
||||||
|
let method = method.unwrap_or_else(|| "GET".to_string());
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let mut request = match method.to_uppercase().as_str() {
|
||||||
|
"GET" => client.get(&url),
|
||||||
|
"POST" => client.post(&url),
|
||||||
|
"PUT" => client.put(&url),
|
||||||
|
"DELETE" => client.delete(&url),
|
||||||
|
"PATCH" => client.patch(&url),
|
||||||
|
_ => {
|
||||||
|
return Err(ExtensionError::ValidationError {
|
||||||
|
reason: format!("Unsupported HTTP method: {}", method),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
if let Some(headers) = headers {
|
||||||
|
if let Some(headers_obj) = headers.as_object() {
|
||||||
|
for (key, value) in headers_obj {
|
||||||
|
if let Some(value_str) = value.as_str() {
|
||||||
|
request = request.header(key, value_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add body
|
||||||
|
if let Some(body) = body {
|
||||||
|
request = request.body(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute request
|
||||||
|
let response = request
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("HTTP request failed: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let status = response.status().as_u16();
|
||||||
|
let headers_map = response.headers().clone();
|
||||||
|
|
||||||
|
// Get response body as bytes
|
||||||
|
let body_bytes = response
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Failed to read response body: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Encode body as base64
|
||||||
|
let body_base64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &body_bytes);
|
||||||
|
|
||||||
|
// Convert headers to JSON
|
||||||
|
let mut headers_json = serde_json::Map::new();
|
||||||
|
for (key, value) in headers_map.iter() {
|
||||||
|
if let Ok(value_str) = value.to_str() {
|
||||||
|
headers_json.insert(
|
||||||
|
key.to_string(),
|
||||||
|
serde_json::Value::String(value_str.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"status": status,
|
||||||
|
"headers": headers_json,
|
||||||
|
"body": body_base64,
|
||||||
|
"ok": status >= 200 && status < 300
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Broadcasts an event to all extension webview windows
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn webview_extension_emit_to_all(
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
event: String,
|
||||||
|
payload: serde_json::Value,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
{
|
||||||
|
state.extension_webview_manager.emit_to_all_extensions(
|
||||||
|
&app_handle,
|
||||||
|
&event,
|
||||||
|
payload,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -1,7 +1,14 @@
|
|||||||
mod crdt;
|
mod crdt;
|
||||||
mod database;
|
mod database;
|
||||||
mod extension;
|
mod extension;
|
||||||
use crate::{crdt::hlc::HlcService, database::DbConnection, extension::core::ExtensionManager};
|
use crate::{
|
||||||
|
crdt::hlc::HlcService,
|
||||||
|
database::DbConnection,
|
||||||
|
extension::core::ExtensionManager,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
use crate::extension::webview::ExtensionWebviewManager;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
@ -9,10 +16,17 @@ pub mod table_names {
|
|||||||
include!(concat!(env!("OUT_DIR"), "/tableNames.rs"));
|
include!(concat!(env!("OUT_DIR"), "/tableNames.rs"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod event_names {
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/eventNames.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db: DbConnection,
|
pub db: DbConnection,
|
||||||
pub hlc: Mutex<HlcService>,
|
pub hlc: Mutex<HlcService>,
|
||||||
pub extension_manager: ExtensionManager,
|
pub extension_manager: ExtensionManager,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub extension_webview_manager: ExtensionWebviewManager,
|
||||||
|
pub context: Arc<Mutex<extension::webview::web::ApplicationContext>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@ -54,6 +68,13 @@ pub fn run() {
|
|||||||
db: DbConnection(Arc::new(Mutex::new(None))),
|
db: DbConnection(Arc::new(Mutex::new(None))),
|
||||||
hlc: Mutex::new(HlcService::new()),
|
hlc: Mutex::new(HlcService::new()),
|
||||||
extension_manager: ExtensionManager::new(),
|
extension_manager: ExtensionManager::new(),
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension_webview_manager: ExtensionWebviewManager::new(),
|
||||||
|
context: Arc::new(Mutex::new(extension::webview::web::ApplicationContext {
|
||||||
|
theme: "dark".to_string(),
|
||||||
|
locale: "en".to_string(),
|
||||||
|
platform: std::env::consts::OS.to_string(),
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
//.manage(ExtensionState::default())
|
//.manage(ExtensionState::default())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
@ -78,6 +99,11 @@ pub fn run() {
|
|||||||
database::vault_exists,
|
database::vault_exists,
|
||||||
extension::database::extension_sql_execute,
|
extension::database::extension_sql_execute,
|
||||||
extension::database::extension_sql_select,
|
extension::database::extension_sql_select,
|
||||||
|
extension::web::extension_web_fetch,
|
||||||
|
extension::web::extension_web_open,
|
||||||
|
extension::permissions::check::check_web_permission,
|
||||||
|
extension::permissions::check::check_database_permission,
|
||||||
|
extension::permissions::check::check_filesystem_permission,
|
||||||
extension::get_all_dev_extensions,
|
extension::get_all_dev_extensions,
|
||||||
extension::get_all_extensions,
|
extension::get_all_extensions,
|
||||||
extension::get_extension_info,
|
extension::get_extension_info,
|
||||||
@ -87,6 +113,41 @@ pub fn run() {
|
|||||||
extension::preview_extension,
|
extension::preview_extension,
|
||||||
extension::remove_dev_extension,
|
extension::remove_dev_extension,
|
||||||
extension::remove_extension,
|
extension::remove_extension,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::open_extension_webview_window,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::close_extension_webview_window,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::focus_extension_webview_window,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::update_extension_webview_window_position,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::update_extension_webview_window_size,
|
||||||
|
// WebView API commands (for native window extensions, desktop only)
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::webview::web::webview_extension_get_info,
|
||||||
|
extension::webview::web::webview_extension_context_get,
|
||||||
|
extension::webview::web::webview_extension_context_set,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::webview::database::webview_extension_db_query,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::webview::database::webview_extension_db_execute,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::webview::web::webview_extension_check_web_permission,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::webview::web::webview_extension_check_database_permission,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::webview::web::webview_extension_check_filesystem_permission,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::webview::web::webview_extension_web_open,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::webview::web::webview_extension_web_request,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::webview::web::webview_extension_emit_to_all,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::webview::filesystem::webview_extension_fs_save_file,
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
extension::webview::filesystem::webview_extension_fs_open_file,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"app": {
|
"app": {
|
||||||
|
"withGlobalTauri": true,
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "haex-hub",
|
"title": "haex-hub",
|
||||||
|
|||||||
@ -86,6 +86,21 @@ const extension = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const handleIframeLoad = () => {
|
const handleIframeLoad = () => {
|
||||||
|
console.log('[ExtensionFrame] Iframe loaded successfully for:', extension.value?.name)
|
||||||
|
|
||||||
|
// Try to inject a test script to see if JavaScript execution works
|
||||||
|
try {
|
||||||
|
if (iframeRef.value?.contentWindow) {
|
||||||
|
console.log('[ExtensionFrame] Iframe has contentWindow access')
|
||||||
|
// This will fail with sandboxed iframes without allow-same-origin
|
||||||
|
console.log('[ExtensionFrame] Iframe origin:', iframeRef.value.contentWindow.location.href)
|
||||||
|
} else {
|
||||||
|
console.warn('[ExtensionFrame] Iframe contentWindow is null/undefined')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[ExtensionFrame] Cannot access iframe content (expected with sandbox):', e)
|
||||||
|
}
|
||||||
|
|
||||||
// Delay the fade-in slightly to allow window animation to mostly complete
|
// Delay the fade-in slightly to allow window animation to mostly complete
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
@ -102,13 +117,28 @@ const sandboxAttributes = computed(() => {
|
|||||||
|
|
||||||
// Generate extension URL
|
// Generate extension URL
|
||||||
const extensionUrl = computed(() => {
|
const extensionUrl = computed(() => {
|
||||||
if (!extension.value) return ''
|
if (!extension.value) {
|
||||||
|
console.log('[ExtensionFrame] No extension found')
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
const { publicKey, name, version, devServerUrl } = extension.value
|
const { publicKey, name, version, devServerUrl } = extension.value
|
||||||
const assetPath = 'index.html'
|
const assetPath = 'index.html'
|
||||||
|
|
||||||
|
console.log('[ExtensionFrame] Generating URL for extension:', {
|
||||||
|
name,
|
||||||
|
publicKey: publicKey?.substring(0, 10) + '...',
|
||||||
|
version,
|
||||||
|
devServerUrl,
|
||||||
|
platform,
|
||||||
|
})
|
||||||
|
|
||||||
if (!publicKey || !name || !version) {
|
if (!publicKey || !name || !version) {
|
||||||
console.error('Missing required extension fields')
|
console.error('[ExtensionFrame] Missing required extension fields:', {
|
||||||
|
hasPublicKey: !!publicKey,
|
||||||
|
hasName: !!name,
|
||||||
|
hasVersion: !!version,
|
||||||
|
})
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +146,9 @@ const extensionUrl = computed(() => {
|
|||||||
if (devServerUrl) {
|
if (devServerUrl) {
|
||||||
const cleanUrl = devServerUrl.replace(/\/$/, '')
|
const cleanUrl = devServerUrl.replace(/\/$/, '')
|
||||||
const cleanPath = assetPath.replace(/^\//, '')
|
const cleanPath = assetPath.replace(/^\//, '')
|
||||||
return cleanPath ? `${cleanUrl}/${cleanPath}` : cleanUrl
|
const url = cleanPath ? `${cleanUrl}/${cleanPath}` : cleanUrl
|
||||||
|
console.log('[ExtensionFrame] Using dev server URL:', url)
|
||||||
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
const extensionInfo = {
|
const extensionInfo = {
|
||||||
@ -126,13 +158,18 @@ const extensionUrl = computed(() => {
|
|||||||
}
|
}
|
||||||
const encodedInfo = btoa(JSON.stringify(extensionInfo))
|
const encodedInfo = btoa(JSON.stringify(extensionInfo))
|
||||||
|
|
||||||
|
let url = ''
|
||||||
if (platform === 'android' || platform === 'windows') {
|
if (platform === 'android' || platform === 'windows') {
|
||||||
// Android: Tauri uses http://{scheme}.localhost format
|
// Android: Tauri uses http://{scheme}.localhost format
|
||||||
return `http://${EXTENSION_PROTOCOL_NAME}.localhost/${encodedInfo}/${assetPath}`
|
url = `http://${EXTENSION_PROTOCOL_NAME}.localhost/${encodedInfo}/${assetPath}`
|
||||||
|
console.log('[ExtensionFrame] Generated Android/Windows URL:', url)
|
||||||
} else {
|
} else {
|
||||||
// Desktop: Use custom protocol with base64 as host
|
// Desktop: Use custom protocol with base64 as host
|
||||||
return `${EXTENSION_PROTOCOL_PREFIX}${encodedInfo}/${assetPath}`
|
url = `${EXTENSION_PROTOCOL_PREFIX}${encodedInfo}/${assetPath}`
|
||||||
|
console.log('[ExtensionFrame] Generated Desktop URL:', url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
})
|
})
|
||||||
|
|
||||||
const retryLoad = () => {
|
const retryLoad = () => {
|
||||||
@ -150,19 +187,28 @@ onMounted(() => {
|
|||||||
// Wait for iframe to be ready
|
// Wait for iframe to be ready
|
||||||
if (iframeRef.value && extension.value) {
|
if (iframeRef.value && extension.value) {
|
||||||
console.log(
|
console.log(
|
||||||
'[ExtensionFrame] Manually registering iframe on mount',
|
'[ExtensionFrame] Component MOUNTED',
|
||||||
extension.value.name,
|
extension.value.name,
|
||||||
'windowId:',
|
'windowId:',
|
||||||
props.windowId,
|
props.windowId,
|
||||||
)
|
)
|
||||||
registerExtensionIFrame(iframeRef.value, extension.value, props.windowId)
|
registerExtensionIFrame(iframeRef.value, extension.value, props.windowId)
|
||||||
|
} else {
|
||||||
|
console.warn('[ExtensionFrame] Component mounted but missing iframe or extension:', {
|
||||||
|
hasIframe: !!iframeRef.value,
|
||||||
|
hasExtension: !!extension.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Explicit cleanup before unmount
|
// Explicit cleanup before unmount
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
console.log('[ExtensionFrame] Component UNMOUNTING', {
|
||||||
|
extensionId: props.extensionId,
|
||||||
|
windowId: props.windowId,
|
||||||
|
hasIframe: !!iframeRef.value,
|
||||||
|
})
|
||||||
if (iframeRef.value) {
|
if (iframeRef.value) {
|
||||||
console.log('[ExtensionFrame] Unregistering iframe on unmount')
|
|
||||||
unregisterExtensionIFrame(iframeRef.value)
|
unregisterExtensionIFrame(iframeRef.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -92,130 +92,142 @@
|
|||||||
:key="window.id"
|
:key="window.id"
|
||||||
>
|
>
|
||||||
<!-- Overview Mode: Teleport to window preview -->
|
<!-- Overview Mode: Teleport to window preview -->
|
||||||
<Teleport
|
<template
|
||||||
v-if="
|
v-if="
|
||||||
windowManager.showWindowOverview &&
|
windowManager.showWindowOverview &&
|
||||||
overviewWindowState.has(window.id)
|
overviewWindowState.has(window.id)
|
||||||
"
|
"
|
||||||
:to="`#window-preview-${window.id}`"
|
|
||||||
>
|
>
|
||||||
<div
|
<Teleport :to="`#window-preview-${window.id}`">
|
||||||
class="absolute origin-top-left"
|
<div
|
||||||
:style="{
|
class="absolute origin-top-left"
|
||||||
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
:style="{
|
||||||
width: `${overviewWindowState.get(window.id)!.width}px`,
|
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
||||||
height: `${overviewWindowState.get(window.id)!.height}px`,
|
width: `${overviewWindowState.get(window.id)!.width}px`,
|
||||||
}"
|
height: `${overviewWindowState.get(window.id)!.height}px`,
|
||||||
>
|
}"
|
||||||
<HaexWindow
|
|
||||||
v-show="
|
|
||||||
windowManager.showWindowOverview || !window.isMinimized
|
|
||||||
"
|
|
||||||
:id="window.id"
|
|
||||||
v-model:x="overviewWindowState.get(window.id)!.x"
|
|
||||||
v-model:y="overviewWindowState.get(window.id)!.y"
|
|
||||||
v-model:width="overviewWindowState.get(window.id)!.width"
|
|
||||||
v-model:height="overviewWindowState.get(window.id)!.height"
|
|
||||||
:title="window.title"
|
|
||||||
:icon="window.icon"
|
|
||||||
:is-active="windowManager.isWindowActive(window.id)"
|
|
||||||
:source-x="window.sourceX"
|
|
||||||
:source-y="window.sourceY"
|
|
||||||
:source-width="window.sourceWidth"
|
|
||||||
:source-height="window.sourceHeight"
|
|
||||||
:is-opening="window.isOpening"
|
|
||||||
:is-closing="window.isClosing"
|
|
||||||
:warning-level="
|
|
||||||
window.type === 'extension' &&
|
|
||||||
availableExtensions.find(
|
|
||||||
(ext) => ext.id === window.sourceId,
|
|
||||||
)?.devServerUrl
|
|
||||||
? 'warning'
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
class="no-swipe"
|
|
||||||
@close="windowManager.closeWindow(window.id)"
|
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
|
||||||
@activate="windowManager.activateWindow(window.id)"
|
|
||||||
@position-changed="
|
|
||||||
(x, y) =>
|
|
||||||
windowManager.updateWindowPosition(window.id, x, y)
|
|
||||||
"
|
|
||||||
@size-changed="
|
|
||||||
(width, height) =>
|
|
||||||
windowManager.updateWindowSize(window.id, width, height)
|
|
||||||
"
|
|
||||||
@drag-start="handleWindowDragStart(window.id)"
|
|
||||||
@drag-end="handleWindowDragEnd"
|
|
||||||
>
|
>
|
||||||
<!-- System Window: Render Vue Component -->
|
<HaexWindow
|
||||||
<component
|
v-show="
|
||||||
:is="getSystemWindowComponent(window.sourceId)"
|
windowManager.showWindowOverview || !window.isMinimized
|
||||||
v-if="window.type === 'system'"
|
"
|
||||||
/>
|
:id="window.id"
|
||||||
|
v-model:x="overviewWindowState.get(window.id)!.x"
|
||||||
|
v-model:y="overviewWindowState.get(window.id)!.y"
|
||||||
|
v-model:width="overviewWindowState.get(window.id)!.width"
|
||||||
|
v-model:height="
|
||||||
|
overviewWindowState.get(window.id)!.height
|
||||||
|
"
|
||||||
|
:title="window.title"
|
||||||
|
:icon="window.icon"
|
||||||
|
:is-active="windowManager.isWindowActive(window.id)"
|
||||||
|
:source-x="window.sourceX"
|
||||||
|
:source-y="window.sourceY"
|
||||||
|
:source-width="window.sourceWidth"
|
||||||
|
:source-height="window.sourceHeight"
|
||||||
|
:is-opening="window.isOpening"
|
||||||
|
:is-closing="window.isClosing"
|
||||||
|
:warning-level="
|
||||||
|
window.type === 'extension' &&
|
||||||
|
availableExtensions.find(
|
||||||
|
(ext) => ext.id === window.sourceId,
|
||||||
|
)?.devServerUrl
|
||||||
|
? 'warning'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
|
class="no-swipe"
|
||||||
|
@close="windowManager.closeWindow(window.id)"
|
||||||
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
|
@activate="windowManager.activateWindow(window.id)"
|
||||||
|
@position-changed="
|
||||||
|
(x, y) =>
|
||||||
|
windowManager.updateWindowPosition(window.id, x, y)
|
||||||
|
"
|
||||||
|
@size-changed="
|
||||||
|
(width, height) =>
|
||||||
|
windowManager.updateWindowSize(
|
||||||
|
window.id,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@drag-start="handleWindowDragStart(window.id)"
|
||||||
|
@drag-end="handleWindowDragEnd"
|
||||||
|
>
|
||||||
|
<!-- System Window: Render Vue Component -->
|
||||||
|
<component
|
||||||
|
:is="getSystemWindowComponent(window.sourceId)"
|
||||||
|
v-if="window.type === 'system'"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Extension Window: Render iFrame -->
|
<!-- Extension Window: Render iFrame -->
|
||||||
<HaexDesktopExtensionFrame
|
<HaexDesktopExtensionFrame
|
||||||
v-else
|
v-else
|
||||||
:extension-id="window.sourceId"
|
:extension-id="window.sourceId"
|
||||||
:window-id="window.id"
|
:window-id="window.id"
|
||||||
/>
|
/>
|
||||||
</HaexWindow>
|
</HaexWindow>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Desktop Mode: Render directly in workspace -->
|
<!-- Desktop Mode: Render directly in workspace -->
|
||||||
<HaexWindow
|
<template v-else>
|
||||||
v-else
|
<HaexWindow
|
||||||
v-show="windowManager.showWindowOverview || !window.isMinimized"
|
v-show="
|
||||||
:id="window.id"
|
windowManager.showWindowOverview || !window.isMinimized
|
||||||
v-model:x="window.x"
|
"
|
||||||
v-model:y="window.y"
|
:id="window.id"
|
||||||
v-model:width="window.width"
|
v-model:x="window.x"
|
||||||
v-model:height="window.height"
|
v-model:y="window.y"
|
||||||
:title="window.title"
|
v-model:width="window.width"
|
||||||
:icon="window.icon"
|
v-model:height="window.height"
|
||||||
:is-active="windowManager.isWindowActive(window.id)"
|
:title="window.title"
|
||||||
:source-x="window.sourceX"
|
:icon="window.icon"
|
||||||
:source-y="window.sourceY"
|
:is-active="windowManager.isWindowActive(window.id)"
|
||||||
:source-width="window.sourceWidth"
|
:source-x="window.sourceX"
|
||||||
:source-height="window.sourceHeight"
|
:source-y="window.sourceY"
|
||||||
:is-opening="window.isOpening"
|
:source-width="window.sourceWidth"
|
||||||
:is-closing="window.isClosing"
|
:source-height="window.sourceHeight"
|
||||||
:warning-level="
|
:is-opening="window.isOpening"
|
||||||
window.type === 'extension' &&
|
:is-closing="window.isClosing"
|
||||||
availableExtensions.find((ext) => ext.id === window.sourceId)
|
:warning-level="
|
||||||
?.devServerUrl
|
window.type === 'extension' &&
|
||||||
? 'warning'
|
availableExtensions.find(
|
||||||
: undefined
|
(ext) => ext.id === window.sourceId,
|
||||||
"
|
)?.devServerUrl
|
||||||
class="no-swipe"
|
? 'warning'
|
||||||
@close="windowManager.closeWindow(window.id)"
|
: undefined
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
"
|
||||||
@activate="windowManager.activateWindow(window.id)"
|
class="no-swipe"
|
||||||
@position-changed="
|
@close="windowManager.closeWindow(window.id)"
|
||||||
(x, y) => windowManager.updateWindowPosition(window.id, x, y)
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
"
|
@activate="windowManager.activateWindow(window.id)"
|
||||||
@size-changed="
|
@position-changed="
|
||||||
(width, height) =>
|
(x, y) =>
|
||||||
windowManager.updateWindowSize(window.id, width, height)
|
windowManager.updateWindowPosition(window.id, x, y)
|
||||||
"
|
"
|
||||||
@drag-start="handleWindowDragStart(window.id)"
|
@size-changed="
|
||||||
@drag-end="handleWindowDragEnd"
|
(width, height) =>
|
||||||
>
|
windowManager.updateWindowSize(window.id, width, height)
|
||||||
<!-- System Window: Render Vue Component -->
|
"
|
||||||
<component
|
@drag-start="handleWindowDragStart(window.id)"
|
||||||
:is="getSystemWindowComponent(window.sourceId)"
|
@drag-end="handleWindowDragEnd"
|
||||||
v-if="window.type === 'system'"
|
>
|
||||||
/>
|
<!-- System Window: Render Vue Component -->
|
||||||
|
<component
|
||||||
|
:is="getSystemWindowComponent(window.sourceId)"
|
||||||
|
v-if="window.type === 'system'"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Extension Window: Render iFrame -->
|
<!-- Extension Window: Render iFrame -->
|
||||||
<HaexDesktopExtensionFrame
|
<HaexDesktopExtensionFrame
|
||||||
v-else
|
v-else
|
||||||
:extension-id="window.sourceId"
|
:extension-id="window.sourceId"
|
||||||
:window-id="window.id"
|
:window-id="window.id"
|
||||||
/>
|
/>
|
||||||
</HaexWindow>
|
</HaexWindow>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</UContextMenu>
|
</UContextMenu>
|
||||||
@ -297,7 +309,7 @@ const currentDraggedItem = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Track mouse position for showing drop target
|
// Track mouse position for showing drop target
|
||||||
const { x: mouseX, y: mouseY } = useMouse()
|
const { x: mouseX } = useMouse()
|
||||||
|
|
||||||
const dropTargetZone = computed(() => {
|
const dropTargetZone = computed(() => {
|
||||||
if (!isDragging.value) return null
|
if (!isDragging.value) return null
|
||||||
|
|||||||
167
src/components/haex/drawer/vault/create.vue
Normal file
167
src/components/haex/drawer/vault/create.vue
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<UiDrawer
|
||||||
|
v-model:open="open"
|
||||||
|
:title="t('title')"
|
||||||
|
:description="t('description')"
|
||||||
|
>
|
||||||
|
<UiButton
|
||||||
|
:label="t('button.label')"
|
||||||
|
:ui="{
|
||||||
|
base: 'px-3 py-2',
|
||||||
|
}"
|
||||||
|
icon="mdi:plus"
|
||||||
|
size="xl"
|
||||||
|
variant="outline"
|
||||||
|
block
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<div class="p-6 flex flex-col min-h-[50vh]">
|
||||||
|
<div class="flex-1 flex items-center justify-center px-4">
|
||||||
|
<UForm
|
||||||
|
:state="vault"
|
||||||
|
class="w-full max-w-md space-y-6"
|
||||||
|
>
|
||||||
|
<UFormField
|
||||||
|
:label="t('vault.label')"
|
||||||
|
name="name"
|
||||||
|
>
|
||||||
|
<UInput
|
||||||
|
v-model="vault.name"
|
||||||
|
icon="mdi:safe"
|
||||||
|
:placeholder="t('vault.placeholder')"
|
||||||
|
autofocus
|
||||||
|
size="xl"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField
|
||||||
|
:label="t('password.label')"
|
||||||
|
name="password"
|
||||||
|
>
|
||||||
|
<UiInput
|
||||||
|
v-model="vault.password"
|
||||||
|
type="password"
|
||||||
|
icon="i-heroicons-key"
|
||||||
|
:placeholder="t('password.placeholder')"
|
||||||
|
size="xl"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
</UForm>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 mt-auto pt-6">
|
||||||
|
<UButton
|
||||||
|
color="neutral"
|
||||||
|
variant="outline"
|
||||||
|
block
|
||||||
|
size="xl"
|
||||||
|
@click="open = false"
|
||||||
|
>
|
||||||
|
{{ t('cancel') }}
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
color="primary"
|
||||||
|
block
|
||||||
|
size="xl"
|
||||||
|
@click="onCreateAsync"
|
||||||
|
>
|
||||||
|
{{ t('create') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UiDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { vaultSchema } from './schema'
|
||||||
|
|
||||||
|
const open = defineModel<boolean>('open', { default: false })
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
useScope: 'local',
|
||||||
|
})
|
||||||
|
|
||||||
|
const vault = reactive<{
|
||||||
|
name: string
|
||||||
|
password: string
|
||||||
|
type: 'password' | 'text'
|
||||||
|
}>({
|
||||||
|
name: 'HaexVault',
|
||||||
|
password: '',
|
||||||
|
type: 'password',
|
||||||
|
})
|
||||||
|
|
||||||
|
const initVault = () => {
|
||||||
|
vault.name = 'HaexVault'
|
||||||
|
vault.password = ''
|
||||||
|
vault.type = 'password'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { createAsync } = useVaultStore()
|
||||||
|
const { add } = useToast()
|
||||||
|
|
||||||
|
const check = ref(false)
|
||||||
|
|
||||||
|
const onCreateAsync = async () => {
|
||||||
|
check.value = true
|
||||||
|
|
||||||
|
const nameCheck = vaultSchema.name.safeParse(vault.name)
|
||||||
|
const passwordCheck = vaultSchema.password.safeParse(vault.password)
|
||||||
|
|
||||||
|
if (!nameCheck.success || !passwordCheck.success) return
|
||||||
|
|
||||||
|
open.value = false
|
||||||
|
try {
|
||||||
|
if (vault.name && vault.password) {
|
||||||
|
const vaultId = await createAsync({
|
||||||
|
vaultName: vault.name,
|
||||||
|
password: vault.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (vaultId) {
|
||||||
|
initVault()
|
||||||
|
await navigateTo(
|
||||||
|
useLocaleRoute()({ name: 'desktop', params: { vaultId } }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
add({ color: 'error', description: JSON.stringify(error) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
de:
|
||||||
|
button:
|
||||||
|
label: Vault erstellen
|
||||||
|
vault:
|
||||||
|
label: Vaultname
|
||||||
|
placeholder: Vaultname
|
||||||
|
password:
|
||||||
|
label: Passwort
|
||||||
|
placeholder: Passwort eingeben
|
||||||
|
title: Neue HaexVault erstellen
|
||||||
|
create: Erstellen
|
||||||
|
cancel: Abbrechen
|
||||||
|
description: Erstelle eine neue Vault für deine Daten
|
||||||
|
|
||||||
|
en:
|
||||||
|
button:
|
||||||
|
label: Create vault
|
||||||
|
vault:
|
||||||
|
label: Vault name
|
||||||
|
placeholder: Vault name
|
||||||
|
password:
|
||||||
|
label: Password
|
||||||
|
placeholder: Enter password
|
||||||
|
title: Create new HaexVault
|
||||||
|
create: Create
|
||||||
|
cancel: Cancel
|
||||||
|
description: Create a new vault for your data
|
||||||
|
</i18n>
|
||||||
@ -1,12 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<UiDialogConfirm
|
<UiDrawer
|
||||||
v-model:open="open"
|
v-model:open="open"
|
||||||
:confirm-label="t('open')"
|
:title="t('title')"
|
||||||
:description="vault.path || path"
|
:description="path || t('description')"
|
||||||
@confirm="onOpenDatabase"
|
|
||||||
>
|
>
|
||||||
<UiButton
|
<UiButton
|
||||||
:label="t('vault.open')"
|
:label="t('button.label')"
|
||||||
:ui="{
|
:ui="{
|
||||||
base: 'px-3 py-2',
|
base: 'px-3 py-2',
|
||||||
}"
|
}"
|
||||||
@ -16,37 +15,63 @@
|
|||||||
block
|
block
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template #title>
|
<template #content>
|
||||||
<i18n-t
|
<div class="p-6 flex flex-col min-h-[50vh]">
|
||||||
keypath="title"
|
<div class="flex-1 flex items-center justify-center px-4">
|
||||||
tag="p"
|
<div class="w-full max-w-md space-y-4">
|
||||||
class="flex gap-x-2 text-wrap"
|
<div
|
||||||
>
|
v-if="path"
|
||||||
<template #haexvault>
|
class="text-sm text-gray-500 dark:text-gray-400"
|
||||||
<UiTextGradient>HaexVault</UiTextGradient>
|
>
|
||||||
</template>
|
<span class="font-medium">{{ t('path.label') }}:</span>
|
||||||
</i18n-t>
|
{{ path }}
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<template #body>
|
<UForm
|
||||||
<UForm
|
:state="vault"
|
||||||
:state="vault"
|
class="w-full"
|
||||||
class="flex flex-col gap-4 w-full h-full justify-center"
|
>
|
||||||
>
|
<UFormField
|
||||||
<UiInputPassword
|
:label="t('password.label')"
|
||||||
v-model="vault.password"
|
name="password"
|
||||||
class="w-full"
|
>
|
||||||
autofocus
|
<UInput
|
||||||
/>
|
v-model="vault.password"
|
||||||
|
type="password"
|
||||||
|
icon="i-heroicons-key"
|
||||||
|
:placeholder="t('password.placeholder')"
|
||||||
|
autofocus
|
||||||
|
size="xl"
|
||||||
|
class="w-full"
|
||||||
|
@keyup.enter="onOpenDatabase"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
</UForm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<UButton
|
<div class="flex gap-3 mt-auto pt-6">
|
||||||
hidden
|
<UButton
|
||||||
type="submit"
|
color="neutral"
|
||||||
@click="onOpenDatabase"
|
variant="outline"
|
||||||
/>
|
block
|
||||||
</UForm>
|
size="xl"
|
||||||
|
@click="open = false"
|
||||||
|
>
|
||||||
|
{{ t('cancel') }}
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
color="primary"
|
||||||
|
block
|
||||||
|
size="xl"
|
||||||
|
@click="onOpenDatabase"
|
||||||
|
>
|
||||||
|
{{ t('open') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UiDialogConfirm>
|
</UiDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -156,7 +181,12 @@ const onOpenDatabase = async () => {
|
|||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
open.value = false
|
open.value = false
|
||||||
if (error?.details?.reason === 'file is not a database') {
|
const errorDetails =
|
||||||
|
error && typeof error === 'object' && 'details' in error
|
||||||
|
? (error as { details?: { reason?: string } }).details
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (errorDetails?.reason === 'file is not a database') {
|
||||||
add({
|
add({
|
||||||
color: 'error',
|
color: 'error',
|
||||||
title: t('error.password.title'),
|
title: t('error.password.title'),
|
||||||
@ -171,25 +201,37 @@ const onOpenDatabase = async () => {
|
|||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
de:
|
de:
|
||||||
|
button:
|
||||||
|
label: Vault öffnen
|
||||||
open: Entsperren
|
open: Entsperren
|
||||||
title: '{haexvault} entsperren'
|
cancel: Abbrechen
|
||||||
password: Passwort
|
title: HaexVault entsperren
|
||||||
vault:
|
path:
|
||||||
open: Vault öffnen
|
label: Pfad
|
||||||
|
password:
|
||||||
|
label: Passwort
|
||||||
|
placeholder: Passwort eingeben
|
||||||
description: Öffne eine vorhandene Vault
|
description: Öffne eine vorhandene Vault
|
||||||
error:
|
error:
|
||||||
|
open: Vault konnte nicht geöffnet werden
|
||||||
password:
|
password:
|
||||||
title: Vault konnte nicht geöffnet werden
|
title: Vault konnte nicht geöffnet werden
|
||||||
description: Bitte üperprüfe das Passwort
|
description: Bitte überprüfe das Passwort
|
||||||
|
|
||||||
en:
|
en:
|
||||||
|
button:
|
||||||
|
label: Open Vault
|
||||||
open: Unlock
|
open: Unlock
|
||||||
title: Unlock {haexvault}
|
cancel: Cancel
|
||||||
password: Passwort
|
title: Unlock HaexVault
|
||||||
|
path:
|
||||||
|
label: Path
|
||||||
|
password:
|
||||||
|
label: Password
|
||||||
|
placeholder: Enter password
|
||||||
description: Open your existing vault
|
description: Open your existing vault
|
||||||
vault:
|
|
||||||
open: Open Vault
|
|
||||||
error:
|
error:
|
||||||
|
open: Vault couldn't be opened
|
||||||
password:
|
password:
|
||||||
title: Vault couldn't be opened
|
title: Vault couldn't be opened
|
||||||
description: Please check your password
|
description: Please check your password
|
||||||
185
src/components/haex/system/debug-logs.vue
Normal file
185
src/components/haex/system/debug-logs.vue
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full bg-default flex flex-col">
|
||||||
|
<!-- Header with controls -->
|
||||||
|
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon
|
||||||
|
name="i-heroicons-bug-ant"
|
||||||
|
class="w-5 h-5"
|
||||||
|
/>
|
||||||
|
<h2 class="text-lg font-semibold">
|
||||||
|
Debug Logs
|
||||||
|
</h2>
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
{{ logs.length }} logs
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton
|
||||||
|
:label="allCopied ? 'Copied!' : 'Copy All'"
|
||||||
|
:color="allCopied ? 'success' : 'primary'"
|
||||||
|
size="sm"
|
||||||
|
@click="copyAllLogs"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
label="Clear Logs"
|
||||||
|
color="error"
|
||||||
|
size="sm"
|
||||||
|
@click="clearLogs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Buttons -->
|
||||||
|
<div class="flex gap-2 p-4 border-b border-gray-200 dark:border-gray-700 overflow-x-auto">
|
||||||
|
<UButton
|
||||||
|
v-for="level in ['all', 'log', 'info', 'warn', 'error', 'debug']"
|
||||||
|
:key="level"
|
||||||
|
:label="level"
|
||||||
|
:color="filter === level ? 'primary' : 'neutral'"
|
||||||
|
size="sm"
|
||||||
|
@click="filter = level as any"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logs Container -->
|
||||||
|
<div
|
||||||
|
ref="logsContainer"
|
||||||
|
class="flex-1 overflow-y-auto p-4 space-y-2 font-mono text-xs"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(log, index) in filteredLogs"
|
||||||
|
:key="index"
|
||||||
|
:class="[
|
||||||
|
'p-3 rounded-lg border-l-4 relative group',
|
||||||
|
log.level === 'error'
|
||||||
|
? 'bg-red-50 dark:bg-red-950/30 border-red-500'
|
||||||
|
: log.level === 'warn'
|
||||||
|
? 'bg-yellow-50 dark:bg-yellow-950/30 border-yellow-500'
|
||||||
|
: log.level === 'info'
|
||||||
|
? 'bg-blue-50 dark:bg-blue-950/30 border-blue-500'
|
||||||
|
: log.level === 'debug'
|
||||||
|
? 'bg-purple-50 dark:bg-purple-950/30 border-purple-500'
|
||||||
|
: 'bg-gray-50 dark:bg-gray-800 border-gray-400',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- Copy Button -->
|
||||||
|
<button
|
||||||
|
class="absolute top-2 right-2 p-1.5 rounded bg-white dark:bg-gray-700 shadow-sm hover:bg-gray-100 dark:hover:bg-gray-600 active:scale-95 transition-all"
|
||||||
|
@click="copyLogToClipboard(log)"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
:name="copiedIndex === index ? 'i-heroicons-check' : 'i-heroicons-clipboard-document'"
|
||||||
|
:class="[
|
||||||
|
'w-4 h-4',
|
||||||
|
copiedIndex === index ? 'text-green-500' : ''
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex items-start gap-2 mb-1">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400 text-[10px] shrink-0">
|
||||||
|
{{ log.timestamp }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'font-semibold text-[10px] uppercase shrink-0',
|
||||||
|
log.level === 'error'
|
||||||
|
? 'text-red-600 dark:text-red-400'
|
||||||
|
: log.level === 'warn'
|
||||||
|
? 'text-yellow-600 dark:text-yellow-400'
|
||||||
|
: log.level === 'info'
|
||||||
|
? 'text-blue-600 dark:text-blue-400'
|
||||||
|
: log.level === 'debug'
|
||||||
|
? 'text-purple-600 dark:text-purple-400'
|
||||||
|
: 'text-gray-600 dark:text-gray-400',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ log.level }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<pre class="whitespace-pre-wrap break-words text-gray-900 dark:text-gray-100 pr-8">{{ log.message }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="filteredLogs.length === 0"
|
||||||
|
class="text-center text-gray-500 py-8"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
name="i-heroicons-document-text"
|
||||||
|
class="w-12 h-12 mx-auto mb-2 opacity-50"
|
||||||
|
/>
|
||||||
|
<p>No logs to display</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { globalConsoleLogs } from '~/plugins/console-interceptor'
|
||||||
|
import type { ConsoleLog } from '~/plugins/console-interceptor'
|
||||||
|
|
||||||
|
const filter = ref<'all' | 'log' | 'info' | 'warn' | 'error' | 'debug'>('all')
|
||||||
|
const logsContainer = ref<HTMLDivElement>()
|
||||||
|
const copiedIndex = ref<number | null>(null)
|
||||||
|
const allCopied = ref(false)
|
||||||
|
|
||||||
|
const { $clearConsoleLogs } = useNuxtApp()
|
||||||
|
const { copy } = useClipboard()
|
||||||
|
|
||||||
|
const logs = computed(() => globalConsoleLogs.value)
|
||||||
|
|
||||||
|
const filteredLogs = computed(() => {
|
||||||
|
if (filter.value === 'all') {
|
||||||
|
return logs.value
|
||||||
|
}
|
||||||
|
return logs.value.filter((log) => log.level === filter.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const clearLogs = () => {
|
||||||
|
if ($clearConsoleLogs) {
|
||||||
|
$clearConsoleLogs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyLogToClipboard = async (log: ConsoleLog) => {
|
||||||
|
const text = `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`
|
||||||
|
await copy(text)
|
||||||
|
|
||||||
|
// Find the index in filteredLogs for visual feedback
|
||||||
|
const index = filteredLogs.value.indexOf(log)
|
||||||
|
copiedIndex.value = index
|
||||||
|
|
||||||
|
// Reset after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
copiedIndex.value = null
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyAllLogs = async () => {
|
||||||
|
const allLogsText = filteredLogs.value
|
||||||
|
.map((log) => `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
await copy(allLogsText)
|
||||||
|
allCopied.value = true
|
||||||
|
|
||||||
|
// Reset after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
allCopied.value = false
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-scroll to bottom when new logs arrive
|
||||||
|
watch(
|
||||||
|
() => logs.value.length,
|
||||||
|
() => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (logsContainer.value) {
|
||||||
|
logsContainer.value.scrollTop = logsContainer.value.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@ -1,135 +0,0 @@
|
|||||||
<template>
|
|
||||||
<UiDialogConfirm
|
|
||||||
:confirm-label="t('create')"
|
|
||||||
@confirm="onCreateAsync"
|
|
||||||
:description="t('description')"
|
|
||||||
>
|
|
||||||
<UiButton
|
|
||||||
:label="t('vault.create')"
|
|
||||||
:ui="{
|
|
||||||
base: 'px-3 py-2',
|
|
||||||
}"
|
|
||||||
icon="mdi:plus"
|
|
||||||
size="xl"
|
|
||||||
variant="outline"
|
|
||||||
block
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template #title>
|
|
||||||
<i18n-t
|
|
||||||
keypath="title"
|
|
||||||
tag="p"
|
|
||||||
class="flex gap-x-2 flex-wrap"
|
|
||||||
>
|
|
||||||
<template #haexvault>
|
|
||||||
<UiTextGradient>HaexVault</UiTextGradient>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<UForm
|
|
||||||
:state="vault"
|
|
||||||
class="flex flex-col gap-4 w-full h-full justify-center"
|
|
||||||
>
|
|
||||||
<UiInput
|
|
||||||
v-model="vault.name"
|
|
||||||
leading-icon="mdi:safe"
|
|
||||||
:label="t('vault.label')"
|
|
||||||
:placeholder="t('vault.placeholder')"
|
|
||||||
/>
|
|
||||||
<UiInputPassword
|
|
||||||
v-model="vault.password"
|
|
||||||
leading-icon="mdi:key-outline"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
hidden
|
|
||||||
type="submit"
|
|
||||||
@click="onCreateAsync"
|
|
||||||
/>
|
|
||||||
</UForm>
|
|
||||||
</template>
|
|
||||||
</UiDialogConfirm>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { vaultSchema } from './schema'
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
useScope: 'local',
|
|
||||||
})
|
|
||||||
|
|
||||||
const vault = reactive<{
|
|
||||||
name: string
|
|
||||||
password: string
|
|
||||||
type: 'password' | 'text'
|
|
||||||
}>({
|
|
||||||
name: 'HaexVault',
|
|
||||||
password: '',
|
|
||||||
type: 'password',
|
|
||||||
})
|
|
||||||
|
|
||||||
const initVault = () => {
|
|
||||||
vault.name = 'HaexVault'
|
|
||||||
vault.password = ''
|
|
||||||
vault.type = 'password'
|
|
||||||
}
|
|
||||||
|
|
||||||
const { createAsync } = useVaultStore()
|
|
||||||
const { add } = useToast()
|
|
||||||
|
|
||||||
const check = ref(false)
|
|
||||||
const open = ref()
|
|
||||||
|
|
||||||
const onCreateAsync = async () => {
|
|
||||||
check.value = true
|
|
||||||
|
|
||||||
const nameCheck = vaultSchema.name.safeParse(vault.name)
|
|
||||||
const passwordCheck = vaultSchema.password.safeParse(vault.password)
|
|
||||||
|
|
||||||
if (!nameCheck.success || !passwordCheck.success) return
|
|
||||||
|
|
||||||
open.value = false
|
|
||||||
try {
|
|
||||||
if (vault.name && vault.password) {
|
|
||||||
const vaultId = await createAsync({
|
|
||||||
vaultName: vault.name,
|
|
||||||
password: vault.password,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (vaultId) {
|
|
||||||
initVault()
|
|
||||||
await navigateTo(
|
|
||||||
useLocaleRoute()({ name: 'desktop', params: { vaultId } }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
add({ color: 'error', description: JSON.stringify(error) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<i18n lang="yaml">
|
|
||||||
de:
|
|
||||||
vault:
|
|
||||||
create: Neue Vault erstellen
|
|
||||||
label: Vaultname
|
|
||||||
placeholder: Vaultname
|
|
||||||
name: HaexVault
|
|
||||||
title: Neue {haexvault} erstellen
|
|
||||||
create: Erstellen
|
|
||||||
description: Erstelle eine neue Vault für deine Daten
|
|
||||||
|
|
||||||
en:
|
|
||||||
vault:
|
|
||||||
create: Create new vault
|
|
||||||
label: Vaultname
|
|
||||||
placeholder: Vaultname
|
|
||||||
name: HaexVault
|
|
||||||
title: Create new {haexvault}
|
|
||||||
create: Create
|
|
||||||
description: Create a new vault for your data
|
|
||||||
</i18n>
|
|
||||||
@ -5,7 +5,6 @@
|
|||||||
:readonly="props.readOnly"
|
:readonly="props.readOnly"
|
||||||
:leading-icon="props.leadingIcon"
|
:leading-icon="props.leadingIcon"
|
||||||
:ui="{ base: 'peer' }"
|
:ui="{ base: 'peer' }"
|
||||||
size="lg"
|
|
||||||
@change="(e) => $emit('change', e)"
|
@change="(e) => $emit('change', e)"
|
||||||
@blur="(e) => $emit('blur', e)"
|
@blur="(e) => $emit('blur', e)"
|
||||||
@keyup="(e: KeyboardEvent) => $emit('keyup', e)"
|
@keyup="(e: KeyboardEvent) => $emit('keyup', e)"
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// composables/extensionMessageHandler.ts
|
// composables/extensionMessageHandler.ts
|
||||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
|
import { HAEXTENSION_METHODS, HAEXTENSION_EVENTS } from '@haexhub/sdk'
|
||||||
import {
|
import {
|
||||||
EXTENSION_PROTOCOL_NAME,
|
EXTENSION_PROTOCOL_NAME,
|
||||||
EXTENSION_PROTOCOL_PREFIX,
|
EXTENSION_PROTOCOL_PREFIX,
|
||||||
@ -7,7 +8,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
handleDatabaseMethodAsync,
|
handleDatabaseMethodAsync,
|
||||||
handleFilesystemMethodAsync,
|
handleFilesystemMethodAsync,
|
||||||
handleHttpMethodAsync,
|
handleWebMethodAsync,
|
||||||
handlePermissionsMethodAsync,
|
handlePermissionsMethodAsync,
|
||||||
handleContextMethodAsync,
|
handleContextMethodAsync,
|
||||||
handleStorageMethodAsync,
|
handleStorageMethodAsync,
|
||||||
@ -27,14 +28,37 @@ const windowIdToWindowMap = new Map<string, Window>()
|
|||||||
const registerGlobalMessageHandler = () => {
|
const registerGlobalMessageHandler = () => {
|
||||||
if (globalHandlerRegistered) return
|
if (globalHandlerRegistered) return
|
||||||
|
|
||||||
|
console.log('[ExtensionHandler] Registering global message handler')
|
||||||
|
|
||||||
window.addEventListener('message', async (event: MessageEvent) => {
|
window.addEventListener('message', async (event: MessageEvent) => {
|
||||||
|
// Log ALL messages first for debugging
|
||||||
|
console.log('[ExtensionHandler] Raw message received:', {
|
||||||
|
origin: event.origin,
|
||||||
|
dataType: typeof event.data,
|
||||||
|
data: event.data,
|
||||||
|
hasSource: !!event.source,
|
||||||
|
})
|
||||||
|
|
||||||
// Ignore console.forward messages - they're handled elsewhere
|
// Ignore console.forward messages - they're handled elsewhere
|
||||||
if (event.data?.type === 'console.forward') {
|
if (event.data?.type === 'console.forward') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle debug messages for Android debugging
|
||||||
|
if (event.data?.type === 'haexhub:debug') {
|
||||||
|
console.log('[ExtensionHandler] DEBUG MESSAGE FROM EXTENSION:', event.data.data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const request = event.data as ExtensionRequest
|
const request = event.data as ExtensionRequest
|
||||||
|
|
||||||
|
console.log('[ExtensionHandler] Processing extension message:', {
|
||||||
|
origin: event.origin,
|
||||||
|
method: request?.method,
|
||||||
|
id: request?.id,
|
||||||
|
hasSource: !!event.source,
|
||||||
|
})
|
||||||
|
|
||||||
// Find extension instance by decoding event.origin (works with sandboxed iframes)
|
// Find extension instance by decoding event.origin (works with sandboxed iframes)
|
||||||
// Origin formats:
|
// Origin formats:
|
||||||
// - Desktop: haex-extension://<base64>
|
// - Desktop: haex-extension://<base64>
|
||||||
@ -157,17 +181,36 @@ const registerGlobalMessageHandler = () => {
|
|||||||
try {
|
try {
|
||||||
let result: unknown
|
let result: unknown
|
||||||
|
|
||||||
if (request.method.startsWith('haextension.context.')) {
|
// Check specific methods first, then use direct routing to handlers
|
||||||
|
if (request.method === HAEXTENSION_METHODS.context.get) {
|
||||||
result = await handleContextMethodAsync(request)
|
result = await handleContextMethodAsync(request)
|
||||||
} else if (request.method.startsWith('haextension.storage.')) {
|
} else if (
|
||||||
|
request.method === HAEXTENSION_METHODS.storage.getItem ||
|
||||||
|
request.method === HAEXTENSION_METHODS.storage.setItem ||
|
||||||
|
request.method === HAEXTENSION_METHODS.storage.removeItem ||
|
||||||
|
request.method === HAEXTENSION_METHODS.storage.clear ||
|
||||||
|
request.method === HAEXTENSION_METHODS.storage.keys
|
||||||
|
) {
|
||||||
result = await handleStorageMethodAsync(request, instance)
|
result = await handleStorageMethodAsync(request, instance)
|
||||||
} else if (request.method.startsWith('haextension.db.')) {
|
} else if (
|
||||||
|
request.method === HAEXTENSION_METHODS.database.query ||
|
||||||
|
request.method === HAEXTENSION_METHODS.database.execute ||
|
||||||
|
request.method === HAEXTENSION_METHODS.database.transaction
|
||||||
|
) {
|
||||||
result = await handleDatabaseMethodAsync(request, instance.extension)
|
result = await handleDatabaseMethodAsync(request, instance.extension)
|
||||||
} else if (request.method.startsWith('haextension.fs.')) {
|
} else if (
|
||||||
|
request.method === HAEXTENSION_METHODS.filesystem.saveFile ||
|
||||||
|
request.method === HAEXTENSION_METHODS.filesystem.openFile ||
|
||||||
|
request.method === HAEXTENSION_METHODS.filesystem.showImage
|
||||||
|
) {
|
||||||
result = await handleFilesystemMethodAsync(request, instance.extension)
|
result = await handleFilesystemMethodAsync(request, instance.extension)
|
||||||
} else if (request.method.startsWith('haextension.http.')) {
|
} else if (
|
||||||
result = await handleHttpMethodAsync(request, instance.extension)
|
request.method === HAEXTENSION_METHODS.web.fetch ||
|
||||||
} else if (request.method.startsWith('haextension.permissions.')) {
|
request.method === HAEXTENSION_METHODS.application.open
|
||||||
|
) {
|
||||||
|
result = await handleWebMethodAsync(request, instance.extension)
|
||||||
|
} else if (request.method.startsWith('haextension:permissions:')) {
|
||||||
|
// Permissions noch nicht migriert
|
||||||
result = await handlePermissionsMethodAsync(request, instance.extension)
|
result = await handlePermissionsMethodAsync(request, instance.extension)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown method: ${request.method}`)
|
throw new Error(`Unknown method: ${request.method}`)
|
||||||
@ -317,7 +360,7 @@ export const broadcastContextToAllExtensions = (context: {
|
|||||||
platform?: string
|
platform?: string
|
||||||
}) => {
|
}) => {
|
||||||
const message = {
|
const message = {
|
||||||
type: 'haextension.context.changed',
|
type: HAEXTENSION_EVENTS.CONTEXT_CHANGED,
|
||||||
data: { context },
|
data: { context },
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { Platform } from '@tauri-apps/plugin-os'
|
import type { Platform } from '@tauri-apps/plugin-os'
|
||||||
|
import { HAEXTENSION_METHODS } from '@haexhub/sdk'
|
||||||
import type { ExtensionRequest } from './types'
|
import type { ExtensionRequest } from './types'
|
||||||
|
|
||||||
// Context getters are set from the main handler during initialization
|
// Context getters are set from the main handler during initialization
|
||||||
@ -18,7 +19,7 @@ export function setContextGetters(getters: {
|
|||||||
|
|
||||||
export async function handleContextMethodAsync(request: ExtensionRequest) {
|
export async function handleContextMethodAsync(request: ExtensionRequest) {
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'haextension.context.get':
|
case HAEXTENSION_METHODS.context.get:
|
||||||
if (!contextGetters) {
|
if (!contextGetters) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Context not initialized. Make sure useExtensionMessageHandler is called in a component.',
|
'Context not initialized. Make sure useExtensionMessageHandler is called in a component.',
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { HAEXTENSION_METHODS } from '@haexhub/sdk'
|
||||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
import type { ExtensionRequest } from './types'
|
import type { ExtensionRequest } from './types'
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ export async function handleDatabaseMethodAsync(
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'haextension.db.query': {
|
case HAEXTENSION_METHODS.database.query: {
|
||||||
try {
|
try {
|
||||||
const rows = await invoke<unknown[]>('extension_sql_select', {
|
const rows = await invoke<unknown[]>('extension_sql_select', {
|
||||||
sql: params.query || '',
|
sql: params.query || '',
|
||||||
@ -47,7 +48,7 @@ export async function handleDatabaseMethodAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'haextension.db.execute': {
|
case HAEXTENSION_METHODS.database.execute: {
|
||||||
const rows = await invoke<unknown[]>('extension_sql_execute', {
|
const rows = await invoke<unknown[]>('extension_sql_execute', {
|
||||||
sql: params.query || '',
|
sql: params.query || '',
|
||||||
params: params.params || [],
|
params: params.params || [],
|
||||||
@ -62,7 +63,7 @@ export async function handleDatabaseMethodAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'haextension.db.transaction': {
|
case HAEXTENSION_METHODS.database.transaction: {
|
||||||
const statements =
|
const statements =
|
||||||
(request.params as { statements?: string[] }).statements || []
|
(request.params as { statements?: string[] }).statements || []
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { save } from '@tauri-apps/plugin-dialog'
|
|||||||
import { writeFile } from '@tauri-apps/plugin-fs'
|
import { writeFile } from '@tauri-apps/plugin-fs'
|
||||||
import { openPath } from '@tauri-apps/plugin-opener'
|
import { openPath } from '@tauri-apps/plugin-opener'
|
||||||
import { tempDir, join } from '@tauri-apps/api/path'
|
import { tempDir, join } from '@tauri-apps/api/path'
|
||||||
|
import { HAEXTENSION_METHODS } from '@haexhub/sdk'
|
||||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
import type { ExtensionRequest } from './types'
|
import type { ExtensionRequest } from './types'
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ export async function handleFilesystemMethodAsync(
|
|||||||
if (!request || !extension) return
|
if (!request || !extension) return
|
||||||
|
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'haextension.fs.saveFile': {
|
case HAEXTENSION_METHODS.filesystem.saveFile: {
|
||||||
const params = request.params as {
|
const params = request.params as {
|
||||||
data: number[]
|
data: number[]
|
||||||
defaultPath?: string
|
defaultPath?: string
|
||||||
@ -44,7 +45,7 @@ export async function handleFilesystemMethodAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'haextension.fs.showImage': {
|
case HAEXTENSION_METHODS.filesystem.showImage: {
|
||||||
// This method is now handled by the frontend using PhotoSwipe
|
// This method is now handled by the frontend using PhotoSwipe
|
||||||
// We keep it for backwards compatibility but it's a no-op
|
// We keep it for backwards compatibility but it's a no-op
|
||||||
return {
|
return {
|
||||||
@ -53,7 +54,7 @@ export async function handleFilesystemMethodAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'haextension.fs.openFile': {
|
case HAEXTENSION_METHODS.filesystem.openFile: {
|
||||||
const params = request.params as {
|
const params = request.params as {
|
||||||
data: number[]
|
data: number[]
|
||||||
fileName: string
|
fileName: string
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
|
||||||
import type { ExtensionRequest } from './types'
|
|
||||||
|
|
||||||
export async function handleHttpMethodAsync(
|
|
||||||
request: ExtensionRequest,
|
|
||||||
extension: IHaexHubExtension,
|
|
||||||
) {
|
|
||||||
if (!extension || !request) {
|
|
||||||
throw new Error('Extension not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implementiere HTTP Commands im Backend
|
|
||||||
throw new Error('HTTP methods not yet implemented')
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// Export all handler functions
|
// Export all handler functions
|
||||||
export { handleDatabaseMethodAsync } from './database'
|
export { handleDatabaseMethodAsync } from './database'
|
||||||
export { handleFilesystemMethodAsync } from './filesystem'
|
export { handleFilesystemMethodAsync } from './filesystem'
|
||||||
export { handleHttpMethodAsync } from './http'
|
export { handleWebMethodAsync } from './web'
|
||||||
export { handlePermissionsMethodAsync } from './permissions'
|
export { handlePermissionsMethodAsync } from './permissions'
|
||||||
export { handleContextMethodAsync, setContextGetters } from './context'
|
export { handleContextMethodAsync, setContextGetters } from './context'
|
||||||
export { handleStorageMethodAsync } from './storage'
|
export { handleStorageMethodAsync } from './storage'
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
import type { ExtensionRequest } from './types'
|
import type { ExtensionRequest } from './types'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
export async function handlePermissionsMethodAsync(
|
export async function handlePermissionsMethodAsync(
|
||||||
request: ExtensionRequest,
|
request: ExtensionRequest,
|
||||||
@ -9,6 +10,102 @@ export async function handlePermissionsMethodAsync(
|
|||||||
throw new Error('Extension not found')
|
throw new Error('Extension not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implementiere Permission Request UI
|
const { method, params } = request
|
||||||
throw new Error('Permission methods not yet implemented')
|
|
||||||
|
if (method === 'permissions.web.check') {
|
||||||
|
return await checkWebPermissionAsync(params, extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'permissions.database.check') {
|
||||||
|
return await checkDatabasePermissionAsync(params, extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'permissions.filesystem.check') {
|
||||||
|
return await checkFilesystemPermissionAsync(params, extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown permission method: ${method}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkWebPermissionAsync(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
const url = params.url as string
|
||||||
|
const method = (params.method as string) || 'GET'
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new Error('URL is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke<void>('check_web_permission', {
|
||||||
|
extensionId: extension.id,
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { status: 'granted' }
|
||||||
|
} catch (error: any) {
|
||||||
|
// Permission denied errors return a specific error code
|
||||||
|
if (error?.code === 1002 || error?.message?.includes('Permission denied')) {
|
||||||
|
return { status: 'denied' }
|
||||||
|
}
|
||||||
|
// Other errors should be thrown
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkDatabasePermissionAsync(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
const resource = params.resource as string
|
||||||
|
const operation = params.operation as string
|
||||||
|
|
||||||
|
if (!resource || !operation) {
|
||||||
|
throw new Error('Resource and operation are required')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke<void>('check_database_permission', {
|
||||||
|
extensionId: extension.id,
|
||||||
|
resource,
|
||||||
|
operation,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { status: 'granted' }
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.code === 1002 || error?.message?.includes('Permission denied')) {
|
||||||
|
return { status: 'denied' }
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkFilesystemPermissionAsync(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
const path = params.path as string
|
||||||
|
const operation = params.operation as string
|
||||||
|
|
||||||
|
if (!path || !operation) {
|
||||||
|
throw new Error('Path and operation are required')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke<void>('check_filesystem_permission', {
|
||||||
|
extensionId: extension.id,
|
||||||
|
path,
|
||||||
|
operation,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { status: 'granted' }
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.code === 1002 || error?.message?.includes('Permission denied')) {
|
||||||
|
return { status: 'denied' }
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
109
src/composables/handlers/web.ts
Normal file
109
src/composables/handlers/web.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
|
import type { ExtensionRequest } from './types'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { HAEXTENSION_METHODS } from '@haexhub/sdk'
|
||||||
|
|
||||||
|
export async function handleWebMethodAsync(
|
||||||
|
request: ExtensionRequest,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
if (!extension || !request) {
|
||||||
|
throw new Error('Extension not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { method, params } = request
|
||||||
|
|
||||||
|
if (method === HAEXTENSION_METHODS.web.fetch) {
|
||||||
|
return await handleWebFetchAsync(params, extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === HAEXTENSION_METHODS.application.open) {
|
||||||
|
return await handleWebOpenAsync(params, extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown web method: ${method}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWebFetchAsync(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
const url = params.url as string
|
||||||
|
const method = (params.method as string) || undefined
|
||||||
|
const headers = (params.headers as Record<string, string>) || undefined
|
||||||
|
const body = params.body as string | undefined
|
||||||
|
const timeout = (params.timeout as number) || undefined
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new Error('URL is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call Rust backend through Tauri IPC to avoid CORS restrictions
|
||||||
|
const response = await invoke<{
|
||||||
|
status: number
|
||||||
|
status_text: string
|
||||||
|
headers: Record<string, string>
|
||||||
|
body: string
|
||||||
|
url: string
|
||||||
|
}>('extension_web_fetch', {
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
timeout,
|
||||||
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.status_text,
|
||||||
|
headers: response.headers,
|
||||||
|
body: response.body,
|
||||||
|
url: response.url,
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Web request error:', error)
|
||||||
|
|
||||||
|
// Check if it's a permission denied error
|
||||||
|
if (error?.code === 1002 || error?.message?.includes('Permission denied')) {
|
||||||
|
const toast = useToast()
|
||||||
|
toast.add({
|
||||||
|
title: 'Permission denied',
|
||||||
|
description: `Extension "${extension.name}" does not have permission to access ${url}`,
|
||||||
|
color: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new Error(`Web request failed: ${error.message}`)
|
||||||
|
}
|
||||||
|
throw new Error(`Web request failed with unknown error: ${JSON.stringify(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWebOpenAsync(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
const url = params.url as string
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new Error('URL is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call Rust backend to open URL in default browser
|
||||||
|
await invoke<void>('extension_web_open', {
|
||||||
|
url,
|
||||||
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new Error(`Failed to open URL: ${error.message}`)
|
||||||
|
}
|
||||||
|
throw new Error('Failed to open URL with unknown error')
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/constants/eventNames.json
Normal file
5
src/constants/eventNames.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extension": {
|
||||||
|
"windowClosed": "extension-window-closed"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/constants/events.ts
Normal file
9
src/constants/events.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Tauri Event Names
|
||||||
|
* Diese Konstanten werden aus eventNames.json generiert und mit dem Backend synchronisiert
|
||||||
|
*/
|
||||||
|
|
||||||
|
import eventNames from './eventNames.json'
|
||||||
|
|
||||||
|
// Extension Events
|
||||||
|
export const EXTENSION_WINDOW_CLOSED = eventNames.extension.windowClosed
|
||||||
@ -30,9 +30,7 @@ export const haexDevices = sqliteTable(
|
|||||||
id: text(tableNames.haex.devices.columns.id)
|
id: text(tableNames.haex.devices.columns.id)
|
||||||
.$defaultFn(() => crypto.randomUUID())
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.primaryKey(),
|
.primaryKey(),
|
||||||
deviceId: text(tableNames.haex.devices.columns.deviceId)
|
deviceId: text(tableNames.haex.devices.columns.deviceId).notNull().unique(),
|
||||||
.notNull()
|
|
||||||
.unique(),
|
|
||||||
name: text(tableNames.haex.devices.columns.name).notNull(),
|
name: text(tableNames.haex.devices.columns.name).notNull(),
|
||||||
createdAt: text(tableNames.haex.devices.columns.createdAt).default(
|
createdAt: text(tableNames.haex.devices.columns.createdAt).default(
|
||||||
sql`(CURRENT_TIMESTAMP)`,
|
sql`(CURRENT_TIMESTAMP)`,
|
||||||
@ -81,6 +79,7 @@ export const haexExtensions = sqliteTable(
|
|||||||
icon: text(),
|
icon: text(),
|
||||||
signature: text().notNull(),
|
signature: text().notNull(),
|
||||||
single_instance: integer({ mode: 'boolean' }).default(false),
|
single_instance: integer({ mode: 'boolean' }).default(false),
|
||||||
|
display_mode: text().default('auto'),
|
||||||
}),
|
}),
|
||||||
(table) => [
|
(table) => [
|
||||||
// UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren
|
// UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren
|
||||||
@ -102,7 +101,7 @@ export const haexExtensionPermissions = sqliteTable(
|
|||||||
onDelete: 'cascade',
|
onDelete: 'cascade',
|
||||||
}),
|
}),
|
||||||
resourceType: text('resource_type', {
|
resourceType: text('resource_type', {
|
||||||
enum: ['fs', 'http', 'db', 'shell'],
|
enum: ['fs', 'web', 'db', 'shell'],
|
||||||
}),
|
}),
|
||||||
action: text({ enum: ['read', 'write'] }),
|
action: text({ enum: ['read', 'write'] }),
|
||||||
target: text(),
|
target: text(),
|
||||||
|
|||||||
@ -58,6 +58,9 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Page-specific Drawers -->
|
||||||
|
<slot name="drawers" />
|
||||||
|
|
||||||
<!-- Workspace Drawer -->
|
<!-- Workspace Drawer -->
|
||||||
<HaexWorkspaceDrawer />
|
<HaexWorkspaceDrawer />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,10 +15,10 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 h-24 items-stretch justify-center">
|
<div class="flex flex-col gap-4 h-24 items-stretch justify-center">
|
||||||
<HaexVaultCreate />
|
<HaexDrawerVaultCreate v-model:open="isCreateDrawerOpen" />
|
||||||
|
|
||||||
<HaexVaultOpen
|
<HaexDrawerVaultOpen
|
||||||
v-model:open="passwordPromptOpen"
|
v-model:open="isOpenDrawerOpen"
|
||||||
:path="selectedVault?.path"
|
:path="selectedVault?.path"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -54,7 +54,7 @@
|
|||||||
]"
|
]"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
passwordPromptOpen = true
|
isOpenDrawerOpen = true
|
||||||
selectedVault = vault
|
selectedVault = vault
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@ -111,9 +111,23 @@ definePageMeta({
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const passwordPromptOpen = ref(false)
|
const isCreateDrawerOpen = ref(false)
|
||||||
|
const isOpenDrawerOpen = ref(false)
|
||||||
const selectedVault = ref<VaultInfo>()
|
const selectedVault = ref<VaultInfo>()
|
||||||
|
|
||||||
|
// Ensure only one drawer is open at a time
|
||||||
|
watch(isCreateDrawerOpen, (isOpen) => {
|
||||||
|
if (isOpen) {
|
||||||
|
isOpenDrawerOpen.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(isOpenDrawerOpen, (isOpen) => {
|
||||||
|
if (isOpen) {
|
||||||
|
isCreateDrawerOpen.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const showRemoveDialog = ref(false)
|
const showRemoveDialog = ref(false)
|
||||||
|
|
||||||
const { lastVaults } = storeToRefs(useLastVaultStore())
|
const { lastVaults } = storeToRefs(useLastVaultStore())
|
||||||
|
|||||||
@ -57,14 +57,14 @@ function interceptConsole(level: 'log' | 'info' | 'warn' | 'error' | 'debug') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
// TEMPORARILY DISABLED - Console interceptor causes too many logs
|
// Enable console interceptor
|
||||||
// interceptConsole('log')
|
interceptConsole('log')
|
||||||
// interceptConsole('info')
|
interceptConsole('info')
|
||||||
// interceptConsole('warn')
|
interceptConsole('warn')
|
||||||
// interceptConsole('error')
|
interceptConsole('error')
|
||||||
// interceptConsole('debug')
|
interceptConsole('debug')
|
||||||
|
|
||||||
// console.log('[HaexHub] Global console interceptor installed')
|
console.log('[HaexHub] Global console interceptor installed')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provide: {
|
provide: {
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import { defineAsyncComponent, type Component } from 'vue'
|
import { defineAsyncComponent, type Component } from 'vue'
|
||||||
import { getFullscreenDimensions } from '~/utils/viewport'
|
import { getFullscreenDimensions } from '~/utils/viewport'
|
||||||
|
import { isDesktop } from '~/utils/platform'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||||
|
import { EXTENSION_WINDOW_CLOSED } from '~/constants/events'
|
||||||
|
|
||||||
export interface IWindow {
|
export interface IWindow {
|
||||||
id: string
|
id: string
|
||||||
@ -87,6 +91,18 @@ export const useWindowManagerStore = defineStore('windowManager', () => {
|
|||||||
resizable: true,
|
resizable: true,
|
||||||
singleton: false,
|
singleton: false,
|
||||||
},
|
},
|
||||||
|
debugLogs: {
|
||||||
|
id: 'debugLogs',
|
||||||
|
name: 'Debug Logs',
|
||||||
|
icon: 'i-heroicons-bug-ant',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('@/components/haex/system/debug-logs.vue'),
|
||||||
|
),
|
||||||
|
defaultWidth: 1000,
|
||||||
|
defaultHeight: 700,
|
||||||
|
resizable: true,
|
||||||
|
singleton: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSystemWindow = (id: string): SystemWindowDefinition | undefined => {
|
const getSystemWindow = (id: string): SystemWindowDefinition | undefined => {
|
||||||
@ -141,6 +157,73 @@ export const useWindowManagerStore = defineStore('windowManager', () => {
|
|||||||
workspaceId?: string
|
workspaceId?: string
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
// Desktop: Check extension's display_mode preference
|
||||||
|
if (type === 'extension') {
|
||||||
|
const extensionsStore = useExtensionsStore()
|
||||||
|
const extension = extensionsStore.availableExtensions.find(
|
||||||
|
(e) => e.id === sourceId,
|
||||||
|
)
|
||||||
|
const finalTitle = title ?? extension?.name ?? 'Extension'
|
||||||
|
|
||||||
|
// Determine if we should use native window based on display_mode and platform
|
||||||
|
const displayMode = extension?.displayMode ?? 'auto'
|
||||||
|
const shouldUseNativeWindow =
|
||||||
|
(displayMode === 'window') ||
|
||||||
|
(displayMode === 'auto' && isDesktop())
|
||||||
|
|
||||||
|
console.log('[windowManager] Extension display mode check:', {
|
||||||
|
extensionId: sourceId,
|
||||||
|
extensionName: extension?.name,
|
||||||
|
displayMode,
|
||||||
|
isDesktop: isDesktop(),
|
||||||
|
shouldUseNativeWindow,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Desktop: Extensions can run in native WebviewWindows (separate processes)
|
||||||
|
if (isDesktop() && shouldUseNativeWindow) {
|
||||||
|
try {
|
||||||
|
console.log('[windowManager] Opening native window with sourceId:', sourceId)
|
||||||
|
console.log('[windowManager] Extension object:', extension)
|
||||||
|
// Backend generates and returns the window_id
|
||||||
|
const windowId = await invoke<string>('open_extension_webview_window', {
|
||||||
|
extensionId: sourceId,
|
||||||
|
title: finalTitle,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
x: undefined, // Let OS handle positioning
|
||||||
|
y: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Store minimal metadata for tracking (no UI management needed on desktop)
|
||||||
|
const newWindow: IWindow = {
|
||||||
|
id: windowId, // Use window_id from backend as ID
|
||||||
|
workspaceId: '', // Not used on desktop
|
||||||
|
type,
|
||||||
|
sourceId,
|
||||||
|
title: finalTitle,
|
||||||
|
icon,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
isMinimized: false,
|
||||||
|
zIndex: 0,
|
||||||
|
isOpening: false,
|
||||||
|
isClosing: false,
|
||||||
|
}
|
||||||
|
windows.value.push(newWindow)
|
||||||
|
|
||||||
|
return windowId
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open native extension window:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If display_mode is 'iframe' or we're not on desktop, fall through to iframe logic
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile: Full UI-based window management (original logic)
|
||||||
// Wenn kein workspaceId angegeben ist, nutze die current workspace
|
// Wenn kein workspaceId angegeben ist, nutze die current workspace
|
||||||
const targetWorkspaceId =
|
const targetWorkspaceId =
|
||||||
workspaceId || useWorkspaceStore().currentWorkspace?.id
|
workspaceId || useWorkspaceStore().currentWorkspace?.id
|
||||||
@ -274,10 +357,36 @@ export const useWindowManagerStore = defineStore('windowManager', () => {
|
|||||||
* In Zukunft sollte aber vorher ein close event an die Erweiterungen via postMessage geschickt werden,
|
* In Zukunft sollte aber vorher ein close event an die Erweiterungen via postMessage geschickt werden,
|
||||||
* so dass die Erweiterungen darauf reagieren können, um eventuell ungespeicherte Daten zu sichern
|
* so dass die Erweiterungen darauf reagieren können, um eventuell ungespeicherte Daten zu sichern
|
||||||
*****************************************************************************************************/
|
*****************************************************************************************************/
|
||||||
const closeWindow = (windowId: string) => {
|
const closeWindow = async (windowId: string) => {
|
||||||
const window = windows.value.find((w) => w.id === windowId)
|
const window = windows.value.find((w) => w.id === windowId)
|
||||||
if (!window) return
|
if (!window) return
|
||||||
|
|
||||||
|
// Desktop: Close native WebviewWindow for extensions (only if it's actually a native window)
|
||||||
|
// Check if extension is using native window mode (not iframe)
|
||||||
|
if (isDesktop() && window.type === 'extension') {
|
||||||
|
const extensionsStore = useExtensionsStore()
|
||||||
|
const extension = extensionsStore.availableExtensions.find(
|
||||||
|
(e) => e.id === window.sourceId,
|
||||||
|
)
|
||||||
|
const displayMode = extension?.displayMode ?? 'auto'
|
||||||
|
const isNativeWindow =
|
||||||
|
(displayMode === 'window') ||
|
||||||
|
(displayMode === 'auto' && isDesktop())
|
||||||
|
|
||||||
|
// Only try to close native window if it's actually running as native window
|
||||||
|
if (isNativeWindow) {
|
||||||
|
try {
|
||||||
|
await invoke('close_extension_webview_window', { windowId })
|
||||||
|
// Backend will emit event, our listener will update frontend tracking
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to close native extension window:', error)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// If not a native window, fall through to iframe cleanup below
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile: Animated close with iframe cleanup
|
||||||
// Start closing animation
|
// Start closing animation
|
||||||
window.isClosing = true
|
window.isClosing = true
|
||||||
|
|
||||||
@ -358,6 +467,34 @@ export const useWindowManagerStore = defineStore('windowManager', () => {
|
|||||||
return currentWorkspaceWindows.value.filter((w) => w.isMinimized)
|
return currentWorkspaceWindows.value.filter((w) => w.isMinimized)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Desktop: Listen for native window close events from Tauri
|
||||||
|
// Backend is source of truth, frontend is read-only mirror for tracking
|
||||||
|
let _unlistenWindowClosed: UnlistenFn | null = null
|
||||||
|
|
||||||
|
const setupDesktopEventListenersAsync = async () => {
|
||||||
|
if (!isDesktop()) return
|
||||||
|
|
||||||
|
// Listen for native WebviewWindow close events from backend
|
||||||
|
_unlistenWindowClosed = await listen<string>(
|
||||||
|
EXTENSION_WINDOW_CLOSED,
|
||||||
|
(event) => {
|
||||||
|
const windowId = event.payload
|
||||||
|
console.log(`Native extension window closed: ${windowId}`)
|
||||||
|
|
||||||
|
// Remove from frontend tracking (read-only mirror of backend state)
|
||||||
|
const index = windows.value.findIndex((w) => w.id === windowId)
|
||||||
|
if (index !== -1) {
|
||||||
|
windows.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup listeners on store creation (only on desktop)
|
||||||
|
if (isDesktop()) {
|
||||||
|
setupDesktopEventListenersAsync()
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activateWindow,
|
activateWindow,
|
||||||
activeWindowId,
|
activeWindowId,
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { breakpointsTailwind } from '@vueuse/core'
|
import { breakpointsTailwind } from '@vueuse/core'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { HAEXTENSION_EVENTS } from '@haexhub/sdk'
|
||||||
import { broadcastContextToAllExtensions } from '~/composables/extensionMessageHandler'
|
import { broadcastContextToAllExtensions } from '~/composables/extensionMessageHandler'
|
||||||
|
|
||||||
import de from './de.json'
|
import de from './de.json'
|
||||||
@ -61,16 +63,34 @@ export const useUiStore = defineStore('uiStore', () => {
|
|||||||
colorMode.preference = currentThemeName.value
|
colorMode.preference = currentThemeName.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// Broadcast theme and locale changes to extensions
|
// Broadcast theme and locale changes to extensions (including initial state)
|
||||||
watch([currentThemeName, locale], async () => {
|
watch([currentThemeName, locale], async () => {
|
||||||
const deviceStore = useDeviceStore()
|
const deviceStore = useDeviceStore()
|
||||||
const platformValue = await deviceStore.platform
|
const platformValue = await deviceStore.platform
|
||||||
broadcastContextToAllExtensions({
|
const context = {
|
||||||
theme: currentThemeName.value,
|
theme: currentThemeName.value,
|
||||||
locale: locale.value,
|
locale: locale.value,
|
||||||
platform: platformValue,
|
platform: platformValue,
|
||||||
})
|
}
|
||||||
})
|
|
||||||
|
// Broadcast to iframe extensions (existing)
|
||||||
|
broadcastContextToAllExtensions(context)
|
||||||
|
|
||||||
|
// Update Tauri state and emit event for webview extensions
|
||||||
|
try {
|
||||||
|
await invoke('webview_extension_context_set', { context })
|
||||||
|
console.log('[UI Store] Context set in Tauri state:', context)
|
||||||
|
// Broadcast event to all webview extensions
|
||||||
|
await invoke('webview_extension_emit_to_all', {
|
||||||
|
event: HAEXTENSION_EVENTS.CONTEXT_CHANGED,
|
||||||
|
payload: { context }
|
||||||
|
})
|
||||||
|
console.log('[UI Store] Broadcasted context change event to webview extensions:', context)
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error if not running in Tauri (e.g., browser mode)
|
||||||
|
console.debug('[UI Store] Failed to update Tauri context:', error)
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
const viewportHeightWithoutHeader = ref(0)
|
const viewportHeightWithoutHeader = ref(0)
|
||||||
const headerHeight = ref(0)
|
const headerHeight = ref(0)
|
||||||
|
|||||||
64
src/utils/platform.ts
Normal file
64
src/utils/platform.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { platform, type Platform } from '@tauri-apps/plugin-os'
|
||||||
|
|
||||||
|
let cachedPlatform: Platform | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current platform (cached after first call)
|
||||||
|
*/
|
||||||
|
export const getPlatform = (): Platform => {
|
||||||
|
if (!cachedPlatform) {
|
||||||
|
cachedPlatform = platform()
|
||||||
|
}
|
||||||
|
return cachedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running on a desktop platform (Windows, Linux, macOS)
|
||||||
|
*/
|
||||||
|
export const isDesktop = (): boolean => {
|
||||||
|
const p = getPlatform()
|
||||||
|
return p === 'windows' || p === 'linux' || p === 'macos'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running on a mobile platform (Android, iOS)
|
||||||
|
*/
|
||||||
|
export const isMobile = (): boolean => {
|
||||||
|
const p = getPlatform()
|
||||||
|
return p === 'android' || p === 'ios'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running on Android
|
||||||
|
*/
|
||||||
|
export const isAndroid = (): boolean => {
|
||||||
|
return getPlatform() === 'android'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running on iOS
|
||||||
|
*/
|
||||||
|
export const isIOS = (): boolean => {
|
||||||
|
return getPlatform() === 'ios'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running on Windows
|
||||||
|
*/
|
||||||
|
export const isWindows = (): boolean => {
|
||||||
|
return getPlatform() === 'windows'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running on Linux
|
||||||
|
*/
|
||||||
|
export const isLinux = (): boolean => {
|
||||||
|
return getPlatform() === 'linux'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running on macOS
|
||||||
|
*/
|
||||||
|
export const isMacOS = (): boolean => {
|
||||||
|
return getPlatform() === 'macos'
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user