mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-18 06:50:51 +01:00
Compare commits
4 Commits
ef225b281f
...
5ea04a80e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ea04a80e0 | |||
| 65cf2e2c3c | |||
| 68d542b4d7 | |||
| f97cd4ad97 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -26,4 +26,6 @@ dist-ssr
|
||||
src-tauri/target
|
||||
nogit*
|
||||
.claude
|
||||
.output
|
||||
.output
|
||||
target
|
||||
CLAUDE.md
|
||||
13
CLAUDE.md
Normal file
13
CLAUDE.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Verwandte Projekte
|
||||
|
||||
## SDK
|
||||
|
||||
- /home/haex/Projekte/haexhub-sdk
|
||||
|
||||
## Erweiterung HaexPass (Password Manager)
|
||||
|
||||
- /home/haex/Projekte/haex-pass
|
||||
|
||||
# Codingstyle
|
||||
|
||||
- alle asynchronen Funktionen bitte mit Async prependen
|
||||
665
pnpm-lock.yaml
generated
665
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
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, 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, devServerUrl: string | null, };
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ExtensionPermissions } from "./ExtensionPermissions";
|
||||
|
||||
export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | 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, };
|
||||
|
||||
@ -60,11 +60,12 @@ CREATE TABLE `haex_extensions` (
|
||||
`version` text NOT NULL,
|
||||
`author` text,
|
||||
`description` text,
|
||||
`entry` text DEFAULT 'index.html' NOT NULL,
|
||||
`entry` text DEFAULT 'index.html',
|
||||
`homepage` text,
|
||||
`enabled` integer DEFAULT true,
|
||||
`icon` text,
|
||||
`signature` text NOT NULL,
|
||||
`single_instance` integer DEFAULT false,
|
||||
`haex_timestamp` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
@ -94,6 +95,7 @@ CREATE TABLE `haex_settings` (
|
||||
CREATE UNIQUE INDEX `haex_settings_key_type_value_unique` ON `haex_settings` (`key`,`type`,`value`);--> statement-breakpoint
|
||||
CREATE TABLE `haex_workspaces` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`device_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`position` integer DEFAULT 0 NOT NULL,
|
||||
`haex_timestamp` text
|
||||
@ -1 +0,0 @@
|
||||
ALTER TABLE `haex_workspaces` ADD `device_id` text NOT NULL;
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "bcdd9ad3-a87a-4a43-9eba-673f94b10287",
|
||||
"id": "8dc25226-70f9-4d2e-89d4-f3a6b2bdf58d",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"haex_crdt_configs": {
|
||||
@ -411,7 +411,7 @@
|
||||
"name": "entry",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'index.html'"
|
||||
},
|
||||
@ -444,6 +444,14 @@
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"single_instance": {
|
||||
"name": "single_instance",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"haex_timestamp": {
|
||||
"name": "haex_timestamp",
|
||||
"type": "text",
|
||||
@ -619,6 +627,13 @@
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"device_id": {
|
||||
"name": "device_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
|
||||
@ -1,677 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "27735348-b9c5-4bc6-9cc5-dd707ad689b9",
|
||||
"prevId": "bcdd9ad3-a87a-4a43-9eba-673f94b10287",
|
||||
"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_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_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": true,
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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_key_type_value_unique": {
|
||||
"name": "haex_settings_key_type_value_unique",
|
||||
"columns": [
|
||||
"key",
|
||||
"type",
|
||||
"value"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@ -5,15 +5,8 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1761430560028,
|
||||
"tag": "0000_secret_ender_wiggin",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1761581351395,
|
||||
"tag": "0001_late_the_renegades",
|
||||
"when": 1761821821609,
|
||||
"tag": "0000_dashing_night_nurse",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@ -47,11 +47,12 @@ export const haexExtensions = sqliteTable(
|
||||
version: text().notNull(),
|
||||
author: text(),
|
||||
description: text(),
|
||||
entry: text().notNull().default('index.html'),
|
||||
entry: text().default('index.html'),
|
||||
homepage: text(),
|
||||
enabled: integer({ mode: 'boolean' }).default(true),
|
||||
icon: text(),
|
||||
signature: text().notNull(),
|
||||
single_instance: integer({ mode: 'boolean' }).default(false),
|
||||
}),
|
||||
(table) => [
|
||||
// UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren
|
||||
|
||||
Binary file not shown.
@ -85,7 +85,8 @@ impl ColumnInfo {
|
||||
}
|
||||
|
||||
fn is_safe_identifier(name: &str) -> bool {
|
||||
!name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_')
|
||||
// Allow alphanumeric characters, underscores, and hyphens (for extension names like "nuxt-app")
|
||||
!name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-')
|
||||
}
|
||||
|
||||
/// Richtet CRDT-Trigger für eine einzelne Tabelle ein.
|
||||
|
||||
@ -89,8 +89,15 @@ pub fn parse_single_statement(sql: &str) -> Result<Statement, DatabaseError> {
|
||||
/// Utility für SQL-Parsing - parst mehrere SQL-Statements
|
||||
pub fn parse_sql_statements(sql: &str) -> Result<Vec<Statement>, DatabaseError> {
|
||||
let dialect = SQLiteDialect {};
|
||||
Parser::parse_sql(&dialect, sql).map_err(|e| DatabaseError::ParseError {
|
||||
reason: e.to_string(),
|
||||
|
||||
// Normalize whitespace: replace multiple whitespaces (including newlines, tabs) with single space
|
||||
let normalized_sql = sql
|
||||
.split_whitespace()
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ");
|
||||
|
||||
Parser::parse_sql(&dialect, &normalized_sql).map_err(|e| DatabaseError::ParseError {
|
||||
reason: format!("Failed to parse SQL: {}", e),
|
||||
sql: sql.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -66,17 +66,124 @@ impl ExtensionManager {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Helper function to validate path and check for path traversal
|
||||
/// Returns the cleaned path if valid, or None if invalid/not found
|
||||
/// If require_exists is true, returns None if path doesn't exist
|
||||
pub fn validate_path_in_directory(
|
||||
base_dir: &PathBuf,
|
||||
relative_path: &str,
|
||||
require_exists: bool,
|
||||
) -> Result<Option<PathBuf>, ExtensionError> {
|
||||
// Check for path traversal patterns
|
||||
if relative_path.contains("..") {
|
||||
return Err(ExtensionError::SecurityViolation {
|
||||
reason: format!("Path traversal attempt: {}", relative_path),
|
||||
});
|
||||
}
|
||||
|
||||
// Clean the path (same logic as in protocol.rs)
|
||||
let clean_path = relative_path
|
||||
.replace('\\', "/")
|
||||
.trim_start_matches('/')
|
||||
.split('/')
|
||||
.filter(|&part| !part.is_empty() && part != "." && part != "..")
|
||||
.collect::<PathBuf>();
|
||||
|
||||
let full_path = base_dir.join(&clean_path);
|
||||
|
||||
// Check if file/directory exists (if required)
|
||||
if require_exists && !full_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Verify path is within base directory
|
||||
let canonical_base = base_dir
|
||||
.canonicalize()
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
if let Ok(canonical_path) = full_path.canonicalize() {
|
||||
if !canonical_path.starts_with(&canonical_base) {
|
||||
return Err(ExtensionError::SecurityViolation {
|
||||
reason: format!("Path outside base directory: {}", relative_path),
|
||||
});
|
||||
}
|
||||
Ok(Some(canonical_path))
|
||||
} else {
|
||||
// Path doesn't exist yet - still validate it would be within base
|
||||
if full_path.starts_with(&canonical_base) {
|
||||
Ok(Some(full_path))
|
||||
} else {
|
||||
Err(ExtensionError::SecurityViolation {
|
||||
reason: format!("Path outside base directory: {}", relative_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates icon path and falls back to favicon.ico if not specified
|
||||
fn validate_and_resolve_icon_path(
|
||||
extension_dir: &PathBuf,
|
||||
haextension_dir: &str,
|
||||
icon_path: Option<&str>,
|
||||
) -> Result<Option<String>, ExtensionError> {
|
||||
// If icon is specified in manifest, validate it
|
||||
if let Some(icon) = icon_path {
|
||||
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, icon, true)? {
|
||||
return Ok(Some(clean_path.to_string_lossy().to_string()));
|
||||
} else {
|
||||
eprintln!("WARNING: Icon path specified in manifest not found: {}", icon);
|
||||
// Continue to fallback logic
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback 1: Check haextension/favicon.ico
|
||||
let haextension_favicon = format!("{}/favicon.ico", haextension_dir);
|
||||
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()));
|
||||
}
|
||||
|
||||
// Fallback 2: Check public/favicon.ico
|
||||
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()));
|
||||
}
|
||||
|
||||
// No icon found
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest
|
||||
fn extract_and_validate_extension(
|
||||
bytes: Vec<u8>,
|
||||
temp_prefix: &str,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<ExtractedExtension, ExtensionError> {
|
||||
let temp = std::env::temp_dir().join(format!("{}_{}", temp_prefix, uuid::Uuid::new_v4()));
|
||||
// Use app_cache_dir for better Android compatibility
|
||||
let cache_dir = app_handle
|
||||
.path()
|
||||
.app_cache_dir()
|
||||
.map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Cannot get app cache dir: {}", e),
|
||||
})?;
|
||||
|
||||
let temp_id = uuid::Uuid::new_v4();
|
||||
let temp = cache_dir.join(format!("{}_{}", temp_prefix, temp_id));
|
||||
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)
|
||||
fs::write(&zip_file_path, &bytes).map_err(|e| {
|
||||
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
|
||||
})?;
|
||||
|
||||
// Create extraction directory
|
||||
fs::create_dir_all(&temp)
|
||||
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?;
|
||||
|
||||
let mut archive = ZipArchive::new(Cursor::new(bytes)).map_err(|e| {
|
||||
// Open ZIP file from disk (more reliable on Android than from memory)
|
||||
let zip_file = fs::File::open(&zip_file_path).map_err(|e| {
|
||||
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
|
||||
})?;
|
||||
|
||||
let mut archive = ZipArchive::new(zip_file).map_err(|e| {
|
||||
ExtensionError::InstallationFailed {
|
||||
reason: format!("Invalid ZIP: {}", e),
|
||||
}
|
||||
@ -88,6 +195,9 @@ impl ExtensionManager {
|
||||
reason: format!("Cannot extract ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
// Clean up temporary ZIP file
|
||||
let _ = fs::remove_file(&zip_file_path);
|
||||
|
||||
// Read haextension_dir from config if it exists, otherwise use default
|
||||
let config_path = temp.join("haextension.config.json");
|
||||
let haextension_dir = if config_path.exists() {
|
||||
@ -108,42 +218,17 @@ impl ExtensionManager {
|
||||
.unwrap_or("haextension")
|
||||
.to_string();
|
||||
|
||||
// Security: Validate that haextension_dir doesn't contain ".." for path traversal
|
||||
if dir.contains("..") {
|
||||
return Err(ExtensionError::ManifestError {
|
||||
reason: "Invalid haextension_dir: path traversal with '..' not allowed".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
dir
|
||||
} else {
|
||||
"haextension".to_string()
|
||||
};
|
||||
|
||||
// Build the manifest path
|
||||
let manifest_path = temp.join(&haextension_dir).join("manifest.json");
|
||||
|
||||
// Ensure the resolved path is still within temp directory (safety check against path traversal)
|
||||
let canonical_temp = temp.canonicalize()
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
// Only check if manifest_path parent exists to avoid errors
|
||||
if let Some(parent) = manifest_path.parent() {
|
||||
if let Ok(canonical_manifest_dir) = parent.canonicalize() {
|
||||
if !canonical_manifest_dir.starts_with(&canonical_temp) {
|
||||
return Err(ExtensionError::ManifestError {
|
||||
reason: "Security violation: manifest path outside extension directory".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if manifest exists
|
||||
if !manifest_path.exists() {
|
||||
return Err(ExtensionError::ManifestError {
|
||||
// Validate manifest path using helper function
|
||||
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||
let manifest_path = Self::validate_path_in_directory(&temp, &manifest_relative_path, true)?
|
||||
.ok_or_else(|| ExtensionError::ManifestError {
|
||||
reason: format!("manifest.json not found at {}/manifest.json", haextension_dir),
|
||||
});
|
||||
}
|
||||
})?;
|
||||
|
||||
let actual_dir = temp.clone();
|
||||
let manifest_content =
|
||||
@ -151,7 +236,11 @@ impl ExtensionManager {
|
||||
reason: format!("Cannot read manifest: {}", e),
|
||||
})?;
|
||||
|
||||
let 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
|
||||
let validated_icon = Self::validate_and_resolve_icon_path(&actual_dir, &haextension_dir, manifest.icon.as_deref())?;
|
||||
manifest.icon = validated_icon;
|
||||
|
||||
let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
|
||||
ExtensionError::SignatureVerificationFailed {
|
||||
@ -427,9 +516,10 @@ impl ExtensionManager {
|
||||
|
||||
pub async fn preview_extension_internal(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
file_bytes: Vec<u8>,
|
||||
) -> Result<ExtensionPreview, ExtensionError> {
|
||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview")?;
|
||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview", app_handle)?;
|
||||
|
||||
let is_valid_signature = ExtensionCrypto::verify_signature(
|
||||
&extracted.manifest.public_key,
|
||||
@ -454,7 +544,7 @@ impl ExtensionManager {
|
||||
custom_permissions: EditablePermissions,
|
||||
state: &State<'_, AppState>,
|
||||
) -> Result<String, ExtensionError> {
|
||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext")?;
|
||||
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)
|
||||
ExtensionCrypto::verify_signature(
|
||||
@ -525,7 +615,7 @@ impl ExtensionManager {
|
||||
|
||||
// 1. Extension-Eintrag erstellen mit generierter UUID
|
||||
let insert_ext_sql = format!(
|
||||
"INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
TABLE_EXTENSIONS
|
||||
);
|
||||
|
||||
@ -545,6 +635,7 @@ impl ExtensionManager {
|
||||
extracted.manifest.homepage,
|
||||
extracted.manifest.description,
|
||||
true, // enabled
|
||||
extracted.manifest.single_instance.unwrap_or(false),
|
||||
],
|
||||
)?;
|
||||
|
||||
@ -623,7 +714,7 @@ impl ExtensionManager {
|
||||
// Lade alle Daten aus der Datenbank
|
||||
let extensions = with_connection(&state.db, |conn| {
|
||||
let sql = format!(
|
||||
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled FROM {}",
|
||||
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance FROM {}",
|
||||
TABLE_EXTENSIONS
|
||||
);
|
||||
eprintln!("DEBUG: SQL Query before transformation: {}", sql);
|
||||
@ -655,13 +746,16 @@ impl ExtensionManager {
|
||||
})?
|
||||
.to_string(),
|
||||
author: row[3].as_str().map(String::from),
|
||||
entry: row[4].as_str().unwrap_or("index.html").to_string(),
|
||||
entry: row[4].as_str().map(String::from),
|
||||
icon: row[5].as_str().map(String::from),
|
||||
public_key: row[6].as_str().unwrap_or("").to_string(),
|
||||
signature: row[7].as_str().unwrap_or("").to_string(),
|
||||
permissions: ExtensionPermissions::default(),
|
||||
homepage: row[8].as_str().map(String::from),
|
||||
description: row[9].as_str().map(String::from),
|
||||
single_instance: row[11]
|
||||
.as_bool()
|
||||
.or_else(|| row[11].as_i64().map(|v| v != 0)),
|
||||
};
|
||||
|
||||
let enabled = row[10]
|
||||
@ -722,33 +816,12 @@ impl ExtensionManager {
|
||||
Ok(config_content) => {
|
||||
match serde_json::from_str::<serde_json::Value>(&config_content) {
|
||||
Ok(config) => {
|
||||
let dir = config
|
||||
config
|
||||
.get("dev")
|
||||
.and_then(|dev| dev.get("haextension_dir"))
|
||||
.and_then(|dir| dir.as_str())
|
||||
.unwrap_or("haextension")
|
||||
.to_string();
|
||||
|
||||
// Security: Validate that haextension_dir doesn't contain ".."
|
||||
if dir.contains("..") {
|
||||
eprintln!(
|
||||
"DEBUG: Invalid haextension_dir for: {}, contains '..'",
|
||||
extension_id
|
||||
);
|
||||
self.missing_extensions
|
||||
.lock()
|
||||
.map_err(|e| ExtensionError::MutexPoisoned {
|
||||
reason: e.to_string(),
|
||||
})?
|
||||
.push(MissingExtension {
|
||||
id: extension_id.clone(),
|
||||
public_key: extension_data.manifest.public_key.clone(),
|
||||
name: extension_data.manifest.name.clone(),
|
||||
version: extension_data.manifest.version.clone(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
dir
|
||||
.to_string()
|
||||
}
|
||||
Err(_) => "haextension".to_string(),
|
||||
}
|
||||
@ -759,12 +832,14 @@ impl ExtensionManager {
|
||||
"haextension".to_string()
|
||||
};
|
||||
|
||||
// Check if manifest.json exists in the haextension_dir
|
||||
let manifest_path = extension_path.join(&haextension_dir).join("manifest.json");
|
||||
if !manifest_path.exists() {
|
||||
// Validate manifest.json path using helper function
|
||||
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||
if Self::validate_path_in_directory(&extension_path, &manifest_relative_path, true)?
|
||||
.is_none()
|
||||
{
|
||||
eprintln!(
|
||||
"DEBUG: manifest.json missing for: {} at {:?}",
|
||||
extension_id, manifest_path
|
||||
"DEBUG: manifest.json missing or invalid for: {} at {}/manifest.json",
|
||||
extension_id, haextension_dir
|
||||
);
|
||||
self.missing_extensions
|
||||
.lock()
|
||||
|
||||
@ -57,13 +57,20 @@ pub struct ExtensionManifest {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub entry: String,
|
||||
#[serde(default = "default_entry_value")]
|
||||
pub entry: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub public_key: String,
|
||||
pub signature: String,
|
||||
pub permissions: ExtensionPermissions,
|
||||
pub homepage: Option<String>,
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub single_instance: Option<bool>,
|
||||
}
|
||||
|
||||
fn default_entry_value() -> Option<String> {
|
||||
Some("index.html".to_string())
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
@ -172,6 +179,8 @@ pub struct ExtensionInfoResponse {
|
||||
pub description: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub entry: Option<String>,
|
||||
pub single_instance: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dev_server_url: Option<String>,
|
||||
}
|
||||
@ -197,6 +206,8 @@ impl ExtensionInfoResponse {
|
||||
description: extension.manifest.description.clone(),
|
||||
homepage: extension.manifest.homepage.clone(),
|
||||
icon: extension.manifest.icon.clone(),
|
||||
entry: extension.manifest.entry.clone(),
|
||||
single_instance: extension.manifest.single_instance,
|
||||
dev_server_url,
|
||||
})
|
||||
}
|
||||
|
||||
@ -48,7 +48,9 @@ impl ExtensionCrypto {
|
||||
let relative = path.strip_prefix(dir)
|
||||
.unwrap_or(&path)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
.to_string()
|
||||
// Normalisiere Pfad-Separatoren zu Unix-Style (/) für plattformübergreifende Konsistenz
|
||||
.replace('\\', "/");
|
||||
(relative, path)
|
||||
})
|
||||
.collect();
|
||||
@ -56,16 +58,30 @@ impl ExtensionCrypto {
|
||||
// 3. Sortiere nach relativen Pfaden
|
||||
relative_files.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
println!("=== Files to hash ({}): ===", relative_files.len());
|
||||
for (rel, _) in &relative_files {
|
||||
println!(" - {}", rel);
|
||||
}
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
// Canonicalize manifest path for comparison (important on Android where symlinks may differ)
|
||||
// Also ensure the canonical path is still within the allowed directory (security check)
|
||||
let canonical_manifest_path = manifest_path.canonicalize()
|
||||
.unwrap_or_else(|_| manifest_path.to_path_buf());
|
||||
|
||||
// Security: Verify canonical manifest path is still within dir
|
||||
let canonical_dir = dir.canonicalize()
|
||||
.unwrap_or_else(|_| dir.to_path_buf());
|
||||
|
||||
if !canonical_manifest_path.starts_with(&canonical_dir) {
|
||||
return Err(ExtensionError::ManifestError {
|
||||
reason: format!("Manifest path resolves outside of extension directory (potential path traversal)"),
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Inhalte der sortierten Dateien hashen
|
||||
for (_relative, file_path) in relative_files {
|
||||
if file_path == manifest_path {
|
||||
// Canonicalize file_path for comparison
|
||||
let canonical_file_path = file_path.canonicalize()
|
||||
.unwrap_or_else(|_| file_path.clone());
|
||||
|
||||
if canonical_file_path == canonical_manifest_path {
|
||||
// FÜR DIE MANIFEST.JSON:
|
||||
let content_str = fs::read_to_string(&file_path)
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
@ -94,8 +110,12 @@ impl ExtensionCrypto {
|
||||
reason: format!("Failed to serialize manifest: {}", e),
|
||||
}
|
||||
})?;
|
||||
println!("canonical_manifest_content: {}", canonical_manifest_content);
|
||||
hasher.update(canonical_manifest_content.as_bytes());
|
||||
|
||||
// Normalisiere Zeilenenden zu Unix-Style (\n), wie Node.js JSON.stringify es macht
|
||||
// Dies ist wichtig für plattformübergreifende Konsistenz (Desktop vs Android)
|
||||
let normalized_content = canonical_manifest_content.replace("\r\n", "\n");
|
||||
|
||||
hasher.update(normalized_content.as_bytes());
|
||||
} else {
|
||||
// FÜR ALLE ANDEREN DATEIEN:
|
||||
let content =
|
||||
|
||||
@ -64,7 +64,13 @@ impl SqlExecutor {
|
||||
|
||||
// Trigger-Logik für CREATE TABLE
|
||||
if let Statement::CreateTable(create_table_details) = statement {
|
||||
let table_name_str = create_table_details.name.to_string();
|
||||
let raw_name = create_table_details.name.to_string();
|
||||
// Remove quotes from table name
|
||||
let table_name_str = raw_name
|
||||
.trim_matches('"')
|
||||
.trim_matches('`')
|
||||
.to_string();
|
||||
eprintln!("DEBUG: Setting up triggers for table: {}", table_name_str);
|
||||
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
||||
}
|
||||
|
||||
@ -158,7 +164,13 @@ impl SqlExecutor {
|
||||
|
||||
// Trigger-Logik für CREATE TABLE
|
||||
if let Statement::CreateTable(create_table_details) = statement {
|
||||
let table_name_str = create_table_details.name.to_string();
|
||||
let raw_name = create_table_details.name.to_string();
|
||||
// Remove quotes from table name
|
||||
let table_name_str = raw_name
|
||||
.trim_matches('"')
|
||||
.trim_matches('`')
|
||||
.to_string();
|
||||
eprintln!("DEBUG: Setting up triggers for table (RETURNING): {}", table_name_str);
|
||||
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ use crate::crdt::transformer::CrdtTransformer;
|
||||
use crate::crdt::trigger;
|
||||
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::extension::database::executor::SqlExecutor;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::validator::SqlPermissionValidator;
|
||||
use crate::AppState;
|
||||
@ -110,7 +111,7 @@ pub async fn extension_sql_execute(
|
||||
public_key: String,
|
||||
name: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<String>, ExtensionError> {
|
||||
) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
|
||||
// Get extension to retrieve its ID
|
||||
let extension = state
|
||||
.extension_manager
|
||||
@ -129,58 +130,87 @@ pub async fn extension_sql_execute(
|
||||
// SQL parsing
|
||||
let mut ast_vec = parse_sql_statements(sql)?;
|
||||
|
||||
if ast_vec.len() != 1 {
|
||||
return Err(ExtensionError::Database {
|
||||
source: DatabaseError::ExecutionError {
|
||||
sql: sql.to_string(),
|
||||
reason: "extension_sql_execute should only receive a single SQL statement"
|
||||
.to_string(),
|
||||
table: None,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let mut statement = ast_vec.pop().unwrap();
|
||||
|
||||
// Check if statement has RETURNING clause
|
||||
let has_returning = crate::database::core::statement_has_returning(&statement);
|
||||
|
||||
// Database operation
|
||||
with_connection(&state.db, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
|
||||
let transformer = CrdtTransformer::new();
|
||||
let executor = StatementExecutor::new(&tx);
|
||||
|
||||
// Get HLC service reference
|
||||
let hlc_service = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
|
||||
// Generate HLC timestamp
|
||||
let hlc_timestamp = state
|
||||
.hlc
|
||||
.lock()
|
||||
.unwrap()
|
||||
let hlc_timestamp = hlc_service
|
||||
.new_timestamp_and_persist(&tx)
|
||||
.map_err(|e| DatabaseError::HlcError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
// Transform statements
|
||||
let mut modified_schema_tables = HashSet::new();
|
||||
for statement in &mut ast_vec {
|
||||
if let Some(table_name) =
|
||||
transformer.transform_execute_statement(statement, &hlc_timestamp)?
|
||||
{
|
||||
modified_schema_tables.insert(table_name);
|
||||
}
|
||||
}
|
||||
// Transform statement
|
||||
transformer.transform_execute_statement(&mut statement, &hlc_timestamp)?;
|
||||
|
||||
// Convert parameters
|
||||
// Convert parameters to references
|
||||
let sql_values = ValueConverter::convert_params(¶ms)?;
|
||||
let param_refs: Vec<&dyn rusqlite::ToSql> = sql_values.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
|
||||
|
||||
// Execute statements
|
||||
for statement in ast_vec {
|
||||
executor.execute_statement_with_params(&statement, &sql_values)?;
|
||||
let result = if has_returning {
|
||||
// Use query_internal for statements with RETURNING
|
||||
let (_, rows) = SqlExecutor::query_internal_typed(&tx, &hlc_service, &statement.to_string(), ¶m_refs)?;
|
||||
rows
|
||||
} else {
|
||||
// Use execute_internal for statements without RETURNING
|
||||
SqlExecutor::execute_internal_typed(&tx, &hlc_service, &statement.to_string(), ¶m_refs)?;
|
||||
vec![]
|
||||
};
|
||||
|
||||
if let Statement::CreateTable(create_table_details) = statement {
|
||||
let table_name_str = create_table_details.name.to_string();
|
||||
println!(
|
||||
"Table '{}' created by extension, setting up CRDT triggers...",
|
||||
table_name_str
|
||||
);
|
||||
trigger::setup_triggers_for_table(&tx, &table_name_str, false)?;
|
||||
println!(
|
||||
"Triggers for table '{}' successfully created.",
|
||||
table_name_str
|
||||
);
|
||||
}
|
||||
// Handle CREATE TABLE trigger setup
|
||||
if let Statement::CreateTable(ref create_table_details) = statement {
|
||||
// Extract table name and remove quotes (both " and `)
|
||||
let raw_name = create_table_details.name.to_string();
|
||||
println!("DEBUG: Raw table name from AST: {:?}", raw_name);
|
||||
println!("DEBUG: Raw table name chars: {:?}", raw_name.chars().collect::<Vec<_>>());
|
||||
|
||||
let table_name_str = raw_name
|
||||
.trim_matches('"')
|
||||
.trim_matches('`')
|
||||
.to_string();
|
||||
|
||||
println!("DEBUG: Cleaned table name: {:?}", table_name_str);
|
||||
println!("DEBUG: Cleaned table name chars: {:?}", table_name_str.chars().collect::<Vec<_>>());
|
||||
|
||||
println!(
|
||||
"Table '{}' created by extension, setting up CRDT triggers...",
|
||||
table_name_str
|
||||
);
|
||||
trigger::setup_triggers_for_table(&tx, &table_name_str, false)?;
|
||||
println!(
|
||||
"Triggers for table '{}' successfully created.",
|
||||
table_name_str
|
||||
);
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
|
||||
Ok(modified_schema_tables.into_iter().collect())
|
||||
Ok(result)
|
||||
})
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
@ -192,7 +222,7 @@ pub async fn extension_sql_select(
|
||||
public_key: String,
|
||||
name: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<JsonValue>, ExtensionError> {
|
||||
) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
|
||||
// Get extension to retrieve its ID
|
||||
let extension = state
|
||||
.extension_manager
|
||||
@ -229,10 +259,9 @@ pub async fn extension_sql_select(
|
||||
}
|
||||
}
|
||||
|
||||
// Database operation
|
||||
// Database operation - return Vec<Vec<JsonValue>> like sql_select_with_crdt
|
||||
with_connection(&state.db, |conn| {
|
||||
let sql_params = ValueConverter::convert_params(¶ms)?;
|
||||
// Hard Delete: Keine SELECT-Transformation mehr nötig
|
||||
let stmt_to_execute = ast_vec.pop().unwrap();
|
||||
let transformed_sql = stmt_to_execute.to_string();
|
||||
|
||||
@ -245,51 +274,34 @@ pub async fn extension_sql_select(
|
||||
table: None,
|
||||
})?;
|
||||
|
||||
let column_names: Vec<String> = prepared_stmt
|
||||
.column_names()
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
let rows = prepared_stmt
|
||||
.query_map(params_from_iter(sql_params.iter()), |row| {
|
||||
row_to_json_value(row, &column_names)
|
||||
})
|
||||
let num_columns = prepared_stmt.column_count();
|
||||
let mut rows = prepared_stmt
|
||||
.query(params_from_iter(sql_params.iter()))
|
||||
.map_err(|e| DatabaseError::QueryError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for row_result in rows {
|
||||
results.push(row_result.map_err(|e| DatabaseError::RowProcessingError {
|
||||
reason: e.to_string(),
|
||||
})?);
|
||||
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next().map_err(|e| DatabaseError::QueryError {
|
||||
reason: e.to_string(),
|
||||
})? {
|
||||
let mut row_values: Vec<JsonValue> = Vec::new();
|
||||
for i in 0..num_columns {
|
||||
let value_ref = row.get_ref(i).map_err(|e| DatabaseError::QueryError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
let json_value = crate::database::core::convert_value_ref_to_json(value_ref)?;
|
||||
row_values.push(json_value);
|
||||
}
|
||||
result_vec.push(row_values);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
Ok(result_vec)
|
||||
})
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
/// Konvertiert eine SQLite-Zeile zu JSON
|
||||
fn row_to_json_value(
|
||||
row: &rusqlite::Row,
|
||||
columns: &[String],
|
||||
) -> Result<JsonValue, rusqlite::Error> {
|
||||
let mut map = serde_json::Map::new();
|
||||
for (i, col_name) in columns.iter().enumerate() {
|
||||
let value = row.get::<usize, rusqlite::types::Value>(i)?;
|
||||
let json_value = match value {
|
||||
rusqlite::types::Value::Null => JsonValue::Null,
|
||||
rusqlite::types::Value::Integer(i) => json!(i),
|
||||
rusqlite::types::Value::Real(f) => json!(f),
|
||||
rusqlite::types::Value::Text(s) => json!(s),
|
||||
rusqlite::types::Value::Blob(blob) => json!(blob.to_vec()),
|
||||
};
|
||||
map.insert(col_name.clone(), json_value);
|
||||
}
|
||||
Ok(JsonValue::Object(map))
|
||||
}
|
||||
|
||||
/// Validiert Parameter gegen SQL-Platzhalter
|
||||
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/// src-tauri/src/extension/mod.rs
|
||||
use crate::{
|
||||
extension::{
|
||||
core::{EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
||||
core::{manager::ExtensionManager, EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
||||
error::ExtensionError,
|
||||
},
|
||||
AppState,
|
||||
@ -82,12 +82,13 @@ pub async fn get_all_extensions(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn preview_extension(
|
||||
app_handle: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
file_bytes: Vec<u8>,
|
||||
) -> Result<ExtensionPreview, ExtensionError> {
|
||||
state
|
||||
.extension_manager
|
||||
.preview_extension_internal(file_bytes)
|
||||
.preview_extension_internal(&app_handle, file_bytes)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -320,20 +321,19 @@ pub async fn load_dev_extension(
|
||||
}
|
||||
eprintln!("✅ Dev server is reachable");
|
||||
|
||||
// 2. Build path to manifest: <extension_path>/<haextension_dir>/manifest.json
|
||||
let manifest_path = extension_path_buf
|
||||
.join(&haextension_dir)
|
||||
.join("manifest.json");
|
||||
|
||||
// Check if manifest exists
|
||||
if !manifest_path.exists() {
|
||||
return Err(ExtensionError::ManifestError {
|
||||
reason: format!(
|
||||
"Manifest not found at: {}. Make sure you run 'npx @haexhub/sdk init' first.",
|
||||
manifest_path.display()
|
||||
),
|
||||
});
|
||||
}
|
||||
// 2. Validate and build path to manifest: <extension_path>/<haextension_dir>/manifest.json
|
||||
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||
let manifest_path = ExtensionManager::validate_path_in_directory(
|
||||
&extension_path_buf,
|
||||
&manifest_relative_path,
|
||||
true,
|
||||
)?
|
||||
.ok_or_else(|| ExtensionError::ManifestError {
|
||||
reason: format!(
|
||||
"Manifest not found at: {}/manifest.json. Make sure you run 'npx @haexhub/sdk init' first.",
|
||||
haextension_dir
|
||||
),
|
||||
})?;
|
||||
|
||||
// 3. Read and parse manifest
|
||||
let manifest_content =
|
||||
@ -343,9 +343,8 @@ pub async fn load_dev_extension(
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
// 4. Generate a unique ID for dev extension: dev_<public_key_first_8>_<name>
|
||||
let key_prefix = manifest.public_key.chars().take(8).collect::<String>();
|
||||
let extension_id = format!("dev_{}_{}", key_prefix, manifest.name);
|
||||
// 4. Generate a unique ID for dev extension: dev_<public_key>_<name>
|
||||
let extension_id = format!("dev_{}_{}", manifest.public_key, manifest.name);
|
||||
|
||||
// 5. Check if dev extension already exists (allow reload)
|
||||
if let Some(existing) = state
|
||||
|
||||
@ -197,6 +197,30 @@ impl PermissionManager {
|
||||
action: Action,
|
||||
table_name: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
// Remove quotes from table name if present (from SDK's getTableName())
|
||||
let clean_table_name = table_name.trim_matches('"');
|
||||
|
||||
// Auto-allow: Extensions have full access to their own tables
|
||||
// Table format: {publicKey}__{extensionName}__{tableName}
|
||||
// Extension ID format: dev_{publicKey}_{extensionName} or {publicKey}_{extensionName}
|
||||
|
||||
// Get the extension to check if this is its own table
|
||||
let extension = app_state
|
||||
.extension_manager
|
||||
.get_extension(extension_id)
|
||||
.ok_or_else(|| ExtensionError::ValidationError {
|
||||
reason: format!("Extension with ID {} not found", extension_id),
|
||||
})?;
|
||||
|
||||
// Build expected table prefix: {publicKey}__{extensionName}__
|
||||
let expected_prefix = format!("{}__{}__", extension.manifest.public_key, extension.manifest.name);
|
||||
|
||||
if clean_table_name.starts_with(&expected_prefix) {
|
||||
// This is the extension's own table - auto-allow
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Not own table - check explicit permissions
|
||||
let permissions = Self::get_permissions(app_state, extension_id).await?;
|
||||
|
||||
let has_permission = permissions
|
||||
@ -205,7 +229,7 @@ impl PermissionManager {
|
||||
.filter(|perm| perm.resource_type == ResourceType::Db)
|
||||
.filter(|perm| perm.action == action) // action ist nicht mehr Option
|
||||
.any(|perm| {
|
||||
if perm.target != "*" && perm.target != table_name {
|
||||
if perm.target != "*" && perm.target != clean_table_name {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
|
||||
@ -3,6 +3,8 @@ export default defineAppConfig({
|
||||
colors: {
|
||||
primary: 'sky',
|
||||
secondary: 'fuchsia',
|
||||
warning: 'yellow',
|
||||
danger: 'red',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -14,18 +14,44 @@
|
||||
@apply cursor-not-allowed;
|
||||
}
|
||||
|
||||
/* Define safe-area-insets as CSS custom properties for JavaScript access */
|
||||
:root {
|
||||
--safe-area-inset-top: env(safe-area-inset-top, 0px);
|
||||
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--safe-area-inset-left: env(safe-area-inset-left, 0px);
|
||||
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
||||
}
|
||||
|
||||
/* Verhindere Scrolling auf html und body */
|
||||
html {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100dvh;
|
||||
height: 100vh; /* Fallback */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#__nuxt {
|
||||
/* Stellt sicher, dass die App immer die volle Höhe hat */
|
||||
min-height: 100vh;
|
||||
/* Volle Höhe des body */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
/* Sorgt dafür, dass Padding die Höhe nicht sprengt */
|
||||
@apply box-border;
|
||||
/* Safe-Area Paddings auf root element - damit ALLES davon profitiert */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
|
||||
/* Die Safe-Area Paddings */
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
61
src/components/haex/debug/overlay.vue
Normal file
61
src/components/haex/debug/overlay.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="data"
|
||||
class="fixed top-2 right-2 bg-black/90 text-white text-xs p-3 rounded-lg shadow-2xl max-w-sm z-[9999] backdrop-blur-sm"
|
||||
>
|
||||
<div class="flex justify-between items-start gap-3 mb-2">
|
||||
<span class="font-bold text-sm">{{ title }}</span>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class="bg-white/20 hover:bg-white/30 px-2 py-1 rounded text-xs transition-colors"
|
||||
@click="copyToClipboardAsync"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
v-if="dismissible"
|
||||
class="bg-white/20 hover:bg-white/30 px-2 py-1 rounded text-xs transition-colors"
|
||||
@click="handleDismiss"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="text-xs whitespace-pre-wrap font-mono overflow-auto max-h-96">{{ formattedData }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: Record<string, any> | null
|
||||
title?: string
|
||||
dismissible?: boolean
|
||||
}>(),
|
||||
{
|
||||
title: 'Debug Info',
|
||||
dismissible: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
dismiss: []
|
||||
}>()
|
||||
|
||||
const formattedData = computed(() => {
|
||||
if (!props.data) return ''
|
||||
return JSON.stringify(props.data, null, 2)
|
||||
})
|
||||
|
||||
const copyToClipboardAsync = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(formattedData.value)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy debug info:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDismiss = () => {
|
||||
emit('dismiss')
|
||||
}
|
||||
</script>
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
ref="desktopEl"
|
||||
class="w-full h-full relative overflow-hidden"
|
||||
class="absolute inset-0 overflow-hidden"
|
||||
>
|
||||
<Swiper
|
||||
:modules="[SwiperNavigation]"
|
||||
@ -13,7 +13,7 @@
|
||||
:no-swiping="true"
|
||||
no-swiping-class="no-swipe"
|
||||
:allow-touch-move="allowSwipe"
|
||||
class="w-full h-full"
|
||||
class="h-full w-full"
|
||||
direction="vertical"
|
||||
@swiper="onSwiperInit"
|
||||
@slide-change="onSlideChange"
|
||||
@ -115,6 +115,14 @@
|
||||
: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)"
|
||||
@ -164,6 +172,13 @@
|
||||
: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)"
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
v-if="preview?.manifest.icon"
|
||||
class="w-16 h-16 flex-shrink-0"
|
||||
class="w-16 h-16 shrink-0"
|
||||
>
|
||||
<UIcon
|
||||
:name="preview.manifest.icon"
|
||||
@ -184,7 +184,6 @@ const shellPermissions = computed({
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const permissionAccordionItems = computed(() => {
|
||||
const items = []
|
||||
|
||||
|
||||
@ -3,11 +3,17 @@
|
||||
ref="windowEl"
|
||||
:style="windowStyle"
|
||||
:class="[
|
||||
'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden isolate',
|
||||
'border border-gray-200 dark:border-gray-700 transition-all ease-out duration-600 ',
|
||||
'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden',
|
||||
'transition-all ease-out duration-600',
|
||||
'flex flex-col @container',
|
||||
{ 'select-none': isResizingOrDragging },
|
||||
isActive ? 'z-20' : 'z-10',
|
||||
// Border colors based on warning level
|
||||
warningLevel === 'warning'
|
||||
? 'border-2 border-warning-500'
|
||||
: warningLevel === 'danger'
|
||||
? 'border-2 border-danger-500'
|
||||
: 'border border-gray-200 dark:border-gray-700',
|
||||
]"
|
||||
@mousedown="handleActivate"
|
||||
>
|
||||
@ -86,6 +92,7 @@ const props = defineProps<{
|
||||
sourceHeight?: number
|
||||
isOpening?: boolean
|
||||
isClosing?: boolean
|
||||
warningLevel?: 'warning' | 'danger' // Warning indicator (e.g., dev extension, dangerous permissions)
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -315,13 +322,33 @@ const handleMaximize = () => {
|
||||
const bounds = getViewportBounds()
|
||||
|
||||
if (bounds && bounds.width > 0 && bounds.height > 0) {
|
||||
// Get safe-area-insets from CSS variables for debug
|
||||
const safeAreaTop = parseFloat(
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--safe-area-inset-top',
|
||||
) || '0',
|
||||
)
|
||||
const safeAreaBottom = parseFloat(
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--safe-area-inset-bottom',
|
||||
) || '0',
|
||||
)
|
||||
|
||||
// Desktop container uses 'absolute inset-0' which stretches over full viewport
|
||||
// bounds.height = full viewport height (includes header area + safe-areas)
|
||||
// We need to calculate available space properly
|
||||
|
||||
// Get header height from UI store (measured reactively in layout)
|
||||
const uiStore = useUiStore()
|
||||
const headerHeight = uiStore.headerHeight
|
||||
|
||||
x.value = 0
|
||||
y.value = 0
|
||||
y.value = 0 // Start below header and status bar
|
||||
width.value = bounds.width
|
||||
height.value = bounds.height
|
||||
// Height: viewport - header - both safe-areas
|
||||
height.value = bounds.height - headerHeight - safeAreaTop - safeAreaBottom
|
||||
isMaximized.value = true
|
||||
}
|
||||
console.log('handleMaximize', preMaximizeState, bounds)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -368,7 +368,8 @@ async function handleDatabaseMethodAsync(
|
||||
const rows = await invoke<unknown[]>('extension_sql_select', {
|
||||
sql: params.query || '',
|
||||
params: params.params || [],
|
||||
extensionId: extension.id,
|
||||
publicKey: extension.publicKey,
|
||||
name: extension.name,
|
||||
})
|
||||
|
||||
return {
|
||||
@ -379,14 +380,15 @@ async function handleDatabaseMethodAsync(
|
||||
}
|
||||
|
||||
case 'haextension.db.execute': {
|
||||
await invoke<string[]>('extension_sql_execute', {
|
||||
const rows = await invoke<unknown[]>('extension_sql_execute', {
|
||||
sql: params.query || '',
|
||||
params: params.params || [],
|
||||
extensionId: extension.id,
|
||||
publicKey: extension.publicKey,
|
||||
name: extension.name,
|
||||
})
|
||||
|
||||
return {
|
||||
rows: [],
|
||||
rows,
|
||||
rowsAffected: 1,
|
||||
lastInsertId: undefined,
|
||||
}
|
||||
@ -400,7 +402,8 @@ async function handleDatabaseMethodAsync(
|
||||
await invoke('extension_sql_execute', {
|
||||
sql: stmt,
|
||||
params: [],
|
||||
extensionId: extension.id,
|
||||
publicKey: extension.publicKey,
|
||||
name: extension.name,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="w-dvw h-dvh flex flex-col">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<UPageHeader
|
||||
ref="headerEl"
|
||||
as="header"
|
||||
:ui="{
|
||||
root: ['px-8 py-0'],
|
||||
@ -53,7 +54,7 @@
|
||||
</template>
|
||||
</UPageHeader>
|
||||
|
||||
<main class="flex-1 overflow-hidden bg-elevated flex flex-col">
|
||||
<main class="flex-1 overflow-hidden bg-elevated flex flex-col relative">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
@ -93,11 +94,9 @@
|
||||
variant="outline"
|
||||
class="mt-6"
|
||||
@click="handleAddWorkspace"
|
||||
icon="i-heroicons-plus"
|
||||
:label="t('add')"
|
||||
>
|
||||
<template #leading>
|
||||
<UIcon name="i-heroicons-plus" />
|
||||
</template>
|
||||
New Workspace
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -127,6 +126,15 @@ const handleAddWorkspace = async () => {
|
||||
workspaceStore.slideToWorkspace(workspace?.id)
|
||||
})
|
||||
}
|
||||
|
||||
// Measure header height and store it in UI store
|
||||
const headerEl = useTemplateRef('headerEl')
|
||||
const { height } = useElementSize(headerEl)
|
||||
const uiStore = useUiStore()
|
||||
|
||||
watch(height, (newHeight) => {
|
||||
uiStore.headerHeight = newHeight
|
||||
})
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
@ -136,10 +144,12 @@ de:
|
||||
|
||||
header:
|
||||
workspaces: Workspaces
|
||||
add: Workspace hinzufügen
|
||||
en:
|
||||
search:
|
||||
label: Search
|
||||
|
||||
header:
|
||||
workspaces: Workspaces
|
||||
add: Add Workspace
|
||||
</i18n>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<UDashboardPanel resizable>
|
||||
<HaexDesktop />
|
||||
</div>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@ -50,7 +50,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
currentExtension.value.publicKey,
|
||||
currentExtension.value.name,
|
||||
currentExtension.value.version,
|
||||
'index.html',
|
||||
currentExtension.value.entry ?? 'index.html',
|
||||
currentExtension.value.devServerUrl ?? undefined,
|
||||
)
|
||||
})
|
||||
|
||||
@ -69,10 +69,12 @@ export const useUiStore = defineStore('uiStore', () => {
|
||||
})
|
||||
|
||||
const viewportHeightWithoutHeader = ref(0)
|
||||
const headerHeight = ref(0)
|
||||
|
||||
return {
|
||||
availableThemes,
|
||||
viewportHeightWithoutHeader,
|
||||
headerHeight,
|
||||
currentTheme,
|
||||
currentThemeName,
|
||||
defaultTheme,
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
Blocking waiting for file lock on build directory
|
||||
23.313630402s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex_hub_lib"}: cargo::core::compiler::fingerprint: stale: missing "/home/haex/Projekte/haex-hub/src-tauri/src/database/schemas/crdt.ts"
|
||||
23.319711685s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex_hub_lib"}: cargo::core::compiler::fingerprint: fingerprint dirty for haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri)/Check { test: false }/TargetInner { ..: lib_target("haex_hub_lib", ["staticlib", "cdylib", "rlib"], "/home/haex/Projekte/haex-hub/src-tauri/src/lib.rs", Edition2021) }
|
||||
23.319734303s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex_hub_lib"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "build_script_build" })
|
||||
23.322781234s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="build-script-build"}: cargo::core::compiler::fingerprint: fingerprint dirty for haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri)/RunCustomBuild/TargetInner { ..: custom_build_target("build-script-build", "/home/haex/Projekte/haex-hub/src-tauri/build.rs", Edition2021) }
|
||||
23.322837026s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="build-script-build"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleItem(MissingFile("/home/haex/Projekte/haex-hub/src-tauri/src/database/schemas/crdt.ts")))
|
||||
23.335082427s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex_hub_lib"}: cargo::core::compiler::fingerprint: fingerprint dirty for haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri)/Check { test: true }/TargetInner { ..: lib_target("haex_hub_lib", ["staticlib", "cdylib", "rlib"], "/home/haex/Projekte/haex-hub/src-tauri/src/lib.rs", Edition2021) }
|
||||
23.335103454s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex_hub_lib"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "build_script_build" })
|
||||
23.336844253s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex-hub"}: cargo::core::compiler::fingerprint: fingerprint dirty for haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri)/Check { test: false }/TargetInner { name: "haex-hub", doc: true, ..: with_path("/home/haex/Projekte/haex-hub/src-tauri/src/main.rs", Edition2021) }
|
||||
23.336854407s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex-hub"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "haex_hub_lib" })
|
||||
23.338162602s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex-hub"}: cargo::core::compiler::fingerprint: fingerprint dirty for haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri)/Check { test: true }/TargetInner { name: "haex-hub", doc: true, ..: with_path("/home/haex/Projekte/haex-hub/src-tauri/src/main.rs", Edition2021) }
|
||||
23.338170106s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex-hub"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "haex_hub_lib" })
|
||||
Compiling haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri)
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 25.44s
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user