mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-18 23:10:51 +01:00
Compare commits
4 Commits
ef225b281f
...
5ea04a80e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ea04a80e0 | |||
| 65cf2e2c3c | |||
| 68d542b4d7 | |||
| f97cd4ad97 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -27,3 +27,5 @@ src-tauri/target
|
|||||||
nogit*
|
nogit*
|
||||||
.claude
|
.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.
|
// 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.
|
// 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";
|
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,
|
`version` text NOT NULL,
|
||||||
`author` text,
|
`author` text,
|
||||||
`description` text,
|
`description` text,
|
||||||
`entry` text DEFAULT 'index.html' NOT NULL,
|
`entry` text DEFAULT 'index.html',
|
||||||
`homepage` text,
|
`homepage` text,
|
||||||
`enabled` integer DEFAULT true,
|
`enabled` integer DEFAULT true,
|
||||||
`icon` text,
|
`icon` text,
|
||||||
`signature` text NOT NULL,
|
`signature` text NOT NULL,
|
||||||
|
`single_instance` integer DEFAULT false,
|
||||||
`haex_timestamp` text
|
`haex_timestamp` text
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> 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 UNIQUE INDEX `haex_settings_key_type_value_unique` ON `haex_settings` (`key`,`type`,`value`);--> statement-breakpoint
|
||||||
CREATE TABLE `haex_workspaces` (
|
CREATE TABLE `haex_workspaces` (
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`device_id` text NOT NULL,
|
||||||
`name` text NOT NULL,
|
`name` text NOT NULL,
|
||||||
`position` integer DEFAULT 0 NOT NULL,
|
`position` integer DEFAULT 0 NOT NULL,
|
||||||
`haex_timestamp` text
|
`haex_timestamp` text
|
||||||
@ -1 +0,0 @@
|
|||||||
ALTER TABLE `haex_workspaces` ADD `device_id` text NOT NULL;
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "bcdd9ad3-a87a-4a43-9eba-673f94b10287",
|
"id": "8dc25226-70f9-4d2e-89d4-f3a6b2bdf58d",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"tables": {
|
"tables": {
|
||||||
"haex_crdt_configs": {
|
"haex_crdt_configs": {
|
||||||
@ -411,7 +411,7 @@
|
|||||||
"name": "entry",
|
"name": "entry",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": false,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"default": "'index.html'"
|
"default": "'index.html'"
|
||||||
},
|
},
|
||||||
@ -444,6 +444,14 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
|
"single_instance": {
|
||||||
|
"name": "single_instance",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"haex_timestamp": {
|
"haex_timestamp": {
|
||||||
"name": "haex_timestamp",
|
"name": "haex_timestamp",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@ -619,6 +627,13 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
|
"device_id": {
|
||||||
|
"name": "device_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"type": "text",
|
"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,
|
"idx": 0,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1761430560028,
|
"when": 1761821821609,
|
||||||
"tag": "0000_secret_ender_wiggin",
|
"tag": "0000_dashing_night_nurse",
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1761581351395,
|
|
||||||
"tag": "0001_late_the_renegades",
|
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -47,11 +47,12 @@ export const haexExtensions = sqliteTable(
|
|||||||
version: text().notNull(),
|
version: text().notNull(),
|
||||||
author: text(),
|
author: text(),
|
||||||
description: text(),
|
description: text(),
|
||||||
entry: text().notNull().default('index.html'),
|
entry: text().default('index.html'),
|
||||||
homepage: text(),
|
homepage: text(),
|
||||||
enabled: integer({ mode: 'boolean' }).default(true),
|
enabled: integer({ mode: 'boolean' }).default(true),
|
||||||
icon: text(),
|
icon: text(),
|
||||||
signature: text().notNull(),
|
signature: text().notNull(),
|
||||||
|
single_instance: integer({ mode: 'boolean' }).default(false),
|
||||||
}),
|
}),
|
||||||
(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
|
||||||
|
|||||||
Binary file not shown.
@ -85,7 +85,8 @@ impl ColumnInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_safe_identifier(name: &str) -> bool {
|
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.
|
/// 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
|
/// Utility für SQL-Parsing - parst mehrere SQL-Statements
|
||||||
pub fn parse_sql_statements(sql: &str) -> Result<Vec<Statement>, DatabaseError> {
|
pub fn parse_sql_statements(sql: &str) -> Result<Vec<Statement>, DatabaseError> {
|
||||||
let dialect = SQLiteDialect {};
|
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(),
|
sql: sql.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,17 +66,124 @@ impl ExtensionManager {
|
|||||||
Self::default()
|
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
|
/// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest
|
||||||
fn extract_and_validate_extension(
|
fn extract_and_validate_extension(
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
temp_prefix: &str,
|
temp_prefix: &str,
|
||||||
|
app_handle: &AppHandle,
|
||||||
) -> Result<ExtractedExtension, ExtensionError> {
|
) -> 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)
|
fs::create_dir_all(&temp)
|
||||||
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?;
|
.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 {
|
ExtensionError::InstallationFailed {
|
||||||
reason: format!("Invalid ZIP: {}", e),
|
reason: format!("Invalid ZIP: {}", e),
|
||||||
}
|
}
|
||||||
@ -88,6 +195,9 @@ impl ExtensionManager {
|
|||||||
reason: format!("Cannot extract ZIP: {}", e),
|
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
|
// 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() {
|
||||||
@ -108,42 +218,17 @@ impl ExtensionManager {
|
|||||||
.unwrap_or("haextension")
|
.unwrap_or("haextension")
|
||||||
.to_string();
|
.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
|
dir
|
||||||
} else {
|
} else {
|
||||||
"haextension".to_string()
|
"haextension".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build the manifest path
|
// Validate manifest path using helper function
|
||||||
let manifest_path = temp.join(&haextension_dir).join("manifest.json");
|
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||||
|
let manifest_path = Self::validate_path_in_directory(&temp, &manifest_relative_path, true)?
|
||||||
// Ensure the resolved path is still within temp directory (safety check against path traversal)
|
.ok_or_else(|| ExtensionError::ManifestError {
|
||||||
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 {
|
|
||||||
reason: format!("manifest.json not found at {}/manifest.json", haextension_dir),
|
reason: format!("manifest.json not found at {}/manifest.json", haextension_dir),
|
||||||
});
|
})?;
|
||||||
}
|
|
||||||
|
|
||||||
let actual_dir = temp.clone();
|
let actual_dir = temp.clone();
|
||||||
let manifest_content =
|
let manifest_content =
|
||||||
@ -151,7 +236,11 @@ impl ExtensionManager {
|
|||||||
reason: format!("Cannot read manifest: {}", e),
|
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| {
|
let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
|
||||||
ExtensionError::SignatureVerificationFailed {
|
ExtensionError::SignatureVerificationFailed {
|
||||||
@ -427,9 +516,10 @@ impl ExtensionManager {
|
|||||||
|
|
||||||
pub async fn preview_extension_internal(
|
pub async fn preview_extension_internal(
|
||||||
&self,
|
&self,
|
||||||
|
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")?;
|
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,
|
||||||
@ -454,7 +544,7 @@ 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")?;
|
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(
|
||||||
@ -525,7 +615,7 @@ 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 {} (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
|
TABLE_EXTENSIONS
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -545,6 +635,7 @@ impl ExtensionManager {
|
|||||||
extracted.manifest.homepage,
|
extracted.manifest.homepage,
|
||||||
extracted.manifest.description,
|
extracted.manifest.description,
|
||||||
true, // enabled
|
true, // enabled
|
||||||
|
extracted.manifest.single_instance.unwrap_or(false),
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@ -623,7 +714,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 FROM {}",
|
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance FROM {}",
|
||||||
TABLE_EXTENSIONS
|
TABLE_EXTENSIONS
|
||||||
);
|
);
|
||||||
eprintln!("DEBUG: SQL Query before transformation: {}", sql);
|
eprintln!("DEBUG: SQL Query before transformation: {}", sql);
|
||||||
@ -655,13 +746,16 @@ impl ExtensionManager {
|
|||||||
})?
|
})?
|
||||||
.to_string(),
|
.to_string(),
|
||||||
author: row[3].as_str().map(String::from),
|
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),
|
icon: row[5].as_str().map(String::from),
|
||||||
public_key: row[6].as_str().unwrap_or("").to_string(),
|
public_key: row[6].as_str().unwrap_or("").to_string(),
|
||||||
signature: row[7].as_str().unwrap_or("").to_string(),
|
signature: row[7].as_str().unwrap_or("").to_string(),
|
||||||
permissions: ExtensionPermissions::default(),
|
permissions: ExtensionPermissions::default(),
|
||||||
homepage: row[8].as_str().map(String::from),
|
homepage: row[8].as_str().map(String::from),
|
||||||
description: row[9].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]
|
let enabled = row[10]
|
||||||
@ -722,33 +816,12 @@ impl ExtensionManager {
|
|||||||
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) => {
|
||||||
let dir = 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()
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
Err(_) => "haextension".to_string(),
|
Err(_) => "haextension".to_string(),
|
||||||
}
|
}
|
||||||
@ -759,12 +832,14 @@ impl ExtensionManager {
|
|||||||
"haextension".to_string()
|
"haextension".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if manifest.json exists in the haextension_dir
|
// Validate manifest.json path using helper function
|
||||||
let manifest_path = extension_path.join(&haextension_dir).join("manifest.json");
|
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||||
if !manifest_path.exists() {
|
if Self::validate_path_in_directory(&extension_path, &manifest_relative_path, true)?
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"DEBUG: manifest.json missing for: {} at {:?}",
|
"DEBUG: manifest.json missing or invalid for: {} at {}/manifest.json",
|
||||||
extension_id, manifest_path
|
extension_id, haextension_dir
|
||||||
);
|
);
|
||||||
self.missing_extensions
|
self.missing_extensions
|
||||||
.lock()
|
.lock()
|
||||||
|
|||||||
@ -57,13 +57,20 @@ pub struct ExtensionManifest {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
pub entry: String,
|
#[serde(default = "default_entry_value")]
|
||||||
|
pub entry: Option<String>,
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
pub public_key: String,
|
pub public_key: String,
|
||||||
pub signature: String,
|
pub signature: String,
|
||||||
pub permissions: ExtensionPermissions,
|
pub permissions: ExtensionPermissions,
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
pub description: 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 {
|
impl ExtensionManifest {
|
||||||
@ -172,6 +179,8 @@ pub struct ExtensionInfoResponse {
|
|||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
|
pub entry: Option<String>,
|
||||||
|
pub single_instance: Option<bool>,
|
||||||
#[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>,
|
||||||
}
|
}
|
||||||
@ -197,6 +206,8 @@ impl ExtensionInfoResponse {
|
|||||||
description: extension.manifest.description.clone(),
|
description: extension.manifest.description.clone(),
|
||||||
homepage: extension.manifest.homepage.clone(),
|
homepage: extension.manifest.homepage.clone(),
|
||||||
icon: extension.manifest.icon.clone(),
|
icon: extension.manifest.icon.clone(),
|
||||||
|
entry: extension.manifest.entry.clone(),
|
||||||
|
single_instance: extension.manifest.single_instance,
|
||||||
dev_server_url,
|
dev_server_url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,7 +48,9 @@ impl ExtensionCrypto {
|
|||||||
let relative = path.strip_prefix(dir)
|
let relative = path.strip_prefix(dir)
|
||||||
.unwrap_or(&path)
|
.unwrap_or(&path)
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string()
|
||||||
|
// Normalisiere Pfad-Separatoren zu Unix-Style (/) für plattformübergreifende Konsistenz
|
||||||
|
.replace('\\', "/");
|
||||||
(relative, path)
|
(relative, path)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -56,16 +58,30 @@ impl ExtensionCrypto {
|
|||||||
// 3. Sortiere nach relativen Pfaden
|
// 3. Sortiere nach relativen Pfaden
|
||||||
relative_files.sort_by(|a, b| a.0.cmp(&b.0));
|
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();
|
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
|
// 4. Inhalte der sortierten Dateien hashen
|
||||||
for (_relative, file_path) in relative_files {
|
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:
|
// FÜR DIE MANIFEST.JSON:
|
||||||
let content_str = fs::read_to_string(&file_path)
|
let content_str = fs::read_to_string(&file_path)
|
||||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||||
@ -94,8 +110,12 @@ impl ExtensionCrypto {
|
|||||||
reason: format!("Failed to serialize manifest: {}", e),
|
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 {
|
} else {
|
||||||
// FÜR ALLE ANDEREN DATEIEN:
|
// FÜR ALLE ANDEREN DATEIEN:
|
||||||
let content =
|
let content =
|
||||||
|
|||||||
@ -64,7 +64,13 @@ impl SqlExecutor {
|
|||||||
|
|
||||||
// Trigger-Logik für CREATE TABLE
|
// Trigger-Logik für CREATE TABLE
|
||||||
if let Statement::CreateTable(create_table_details) = statement {
|
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)?;
|
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +164,13 @@ impl SqlExecutor {
|
|||||||
|
|
||||||
// Trigger-Logik für CREATE TABLE
|
// Trigger-Logik für CREATE TABLE
|
||||||
if let Statement::CreateTable(create_table_details) = statement {
|
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)?;
|
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::crdt::trigger;
|
||||||
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
|
use crate::extension::database::executor::SqlExecutor;
|
||||||
use crate::extension::error::ExtensionError;
|
use crate::extension::error::ExtensionError;
|
||||||
use crate::extension::permissions::validator::SqlPermissionValidator;
|
use crate::extension::permissions::validator::SqlPermissionValidator;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
@ -110,7 +111,7 @@ pub async fn extension_sql_execute(
|
|||||||
public_key: String,
|
public_key: String,
|
||||||
name: String,
|
name: String,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<Vec<String>, ExtensionError> {
|
) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
|
||||||
// Get extension to retrieve its ID
|
// Get extension to retrieve its ID
|
||||||
let extension = state
|
let extension = state
|
||||||
.extension_manager
|
.extension_manager
|
||||||
@ -129,42 +130,72 @@ pub async fn extension_sql_execute(
|
|||||||
// SQL parsing
|
// SQL parsing
|
||||||
let mut ast_vec = parse_sql_statements(sql)?;
|
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
|
// Database operation
|
||||||
with_connection(&state.db, |conn| {
|
with_connection(&state.db, |conn| {
|
||||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
let transformer = CrdtTransformer::new();
|
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
|
// Generate HLC timestamp
|
||||||
let hlc_timestamp = state
|
let hlc_timestamp = hlc_service
|
||||||
.hlc
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.new_timestamp_and_persist(&tx)
|
.new_timestamp_and_persist(&tx)
|
||||||
.map_err(|e| DatabaseError::HlcError {
|
.map_err(|e| DatabaseError::HlcError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Transform statements
|
// Transform statement
|
||||||
let mut modified_schema_tables = HashSet::new();
|
transformer.transform_execute_statement(&mut statement, &hlc_timestamp)?;
|
||||||
for statement in &mut ast_vec {
|
|
||||||
if let Some(table_name) =
|
|
||||||
transformer.transform_execute_statement(statement, &hlc_timestamp)?
|
|
||||||
{
|
|
||||||
modified_schema_tables.insert(table_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert parameters
|
// Convert parameters to references
|
||||||
let sql_values = ValueConverter::convert_params(¶ms)?;
|
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
|
let result = if has_returning {
|
||||||
for statement in ast_vec {
|
// Use query_internal for statements with RETURNING
|
||||||
executor.execute_statement_with_params(&statement, &sql_values)?;
|
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![]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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<_>>());
|
||||||
|
|
||||||
if let Statement::CreateTable(create_table_details) = statement {
|
|
||||||
let table_name_str = create_table_details.name.to_string();
|
|
||||||
println!(
|
println!(
|
||||||
"Table '{}' created by extension, setting up CRDT triggers...",
|
"Table '{}' created by extension, setting up CRDT triggers...",
|
||||||
table_name_str
|
table_name_str
|
||||||
@ -175,12 +206,11 @@ pub async fn extension_sql_execute(
|
|||||||
table_name_str
|
table_name_str
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Commit transaction
|
// Commit transaction
|
||||||
tx.commit().map_err(DatabaseError::from)?;
|
tx.commit().map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
Ok(modified_schema_tables.into_iter().collect())
|
Ok(result)
|
||||||
})
|
})
|
||||||
.map_err(ExtensionError::from)
|
.map_err(ExtensionError::from)
|
||||||
}
|
}
|
||||||
@ -192,7 +222,7 @@ pub async fn extension_sql_select(
|
|||||||
public_key: String,
|
public_key: String,
|
||||||
name: String,
|
name: String,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<Vec<JsonValue>, ExtensionError> {
|
) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
|
||||||
// Get extension to retrieve its ID
|
// Get extension to retrieve its ID
|
||||||
let extension = state
|
let extension = state
|
||||||
.extension_manager
|
.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| {
|
with_connection(&state.db, |conn| {
|
||||||
let sql_params = ValueConverter::convert_params(¶ms)?;
|
let sql_params = ValueConverter::convert_params(¶ms)?;
|
||||||
// Hard Delete: Keine SELECT-Transformation mehr nötig
|
|
||||||
let stmt_to_execute = ast_vec.pop().unwrap();
|
let stmt_to_execute = ast_vec.pop().unwrap();
|
||||||
let transformed_sql = stmt_to_execute.to_string();
|
let transformed_sql = stmt_to_execute.to_string();
|
||||||
|
|
||||||
@ -245,51 +274,34 @@ pub async fn extension_sql_select(
|
|||||||
table: None,
|
table: None,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let column_names: Vec<String> = prepared_stmt
|
let num_columns = prepared_stmt.column_count();
|
||||||
.column_names()
|
let mut rows = prepared_stmt
|
||||||
.into_iter()
|
.query(params_from_iter(sql_params.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)
|
|
||||||
})
|
|
||||||
.map_err(|e| DatabaseError::QueryError {
|
.map_err(|e| DatabaseError::QueryError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
||||||
for row_result in rows {
|
|
||||||
results.push(row_result.map_err(|e| DatabaseError::RowProcessingError {
|
while let Some(row) = rows.next().map_err(|e| DatabaseError::QueryError {
|
||||||
reason: e.to_string(),
|
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)
|
.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
|
/// Validiert Parameter gegen SQL-Platzhalter
|
||||||
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {
|
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/// src-tauri/src/extension/mod.rs
|
/// src-tauri/src/extension/mod.rs
|
||||||
use crate::{
|
use crate::{
|
||||||
extension::{
|
extension::{
|
||||||
core::{EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
core::{manager::ExtensionManager, EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
||||||
error::ExtensionError,
|
error::ExtensionError,
|
||||||
},
|
},
|
||||||
AppState,
|
AppState,
|
||||||
@ -82,12 +82,13 @@ pub async fn get_all_extensions(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn preview_extension(
|
pub async fn preview_extension(
|
||||||
|
app_handle: AppHandle,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
file_bytes: Vec<u8>,
|
file_bytes: Vec<u8>,
|
||||||
) -> Result<ExtensionPreview, ExtensionError> {
|
) -> Result<ExtensionPreview, ExtensionError> {
|
||||||
state
|
state
|
||||||
.extension_manager
|
.extension_manager
|
||||||
.preview_extension_internal(file_bytes)
|
.preview_extension_internal(&app_handle, file_bytes)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,20 +321,19 @@ pub async fn load_dev_extension(
|
|||||||
}
|
}
|
||||||
eprintln!("✅ Dev server is reachable");
|
eprintln!("✅ Dev server is reachable");
|
||||||
|
|
||||||
// 2. Build path to manifest: <extension_path>/<haextension_dir>/manifest.json
|
// 2. Validate and build path to manifest: <extension_path>/<haextension_dir>/manifest.json
|
||||||
let manifest_path = extension_path_buf
|
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||||
.join(&haextension_dir)
|
let manifest_path = ExtensionManager::validate_path_in_directory(
|
||||||
.join("manifest.json");
|
&extension_path_buf,
|
||||||
|
&manifest_relative_path,
|
||||||
// Check if manifest exists
|
true,
|
||||||
if !manifest_path.exists() {
|
)?
|
||||||
return Err(ExtensionError::ManifestError {
|
.ok_or_else(|| ExtensionError::ManifestError {
|
||||||
reason: format!(
|
reason: format!(
|
||||||
"Manifest not found at: {}. Make sure you run 'npx @haexhub/sdk init' first.",
|
"Manifest not found at: {}/manifest.json. Make sure you run 'npx @haexhub/sdk init' first.",
|
||||||
manifest_path.display()
|
haextension_dir
|
||||||
),
|
),
|
||||||
});
|
})?;
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Read and parse manifest
|
// 3. Read and parse manifest
|
||||||
let manifest_content =
|
let manifest_content =
|
||||||
@ -343,9 +343,8 @@ pub async fn load_dev_extension(
|
|||||||
|
|
||||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||||
|
|
||||||
// 4. Generate a unique ID for dev extension: dev_<public_key_first_8>_<name>
|
// 4. Generate a unique ID for dev extension: dev_<public_key>_<name>
|
||||||
let key_prefix = manifest.public_key.chars().take(8).collect::<String>();
|
let extension_id = format!("dev_{}_{}", manifest.public_key, manifest.name);
|
||||||
let extension_id = format!("dev_{}_{}", key_prefix, manifest.name);
|
|
||||||
|
|
||||||
// 5. Check if dev extension already exists (allow reload)
|
// 5. Check if dev extension already exists (allow reload)
|
||||||
if let Some(existing) = state
|
if let Some(existing) = state
|
||||||
|
|||||||
@ -197,6 +197,30 @@ impl PermissionManager {
|
|||||||
action: Action,
|
action: Action,
|
||||||
table_name: &str,
|
table_name: &str,
|
||||||
) -> Result<(), ExtensionError> {
|
) -> 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 permissions = Self::get_permissions(app_state, extension_id).await?;
|
||||||
|
|
||||||
let has_permission = permissions
|
let has_permission = permissions
|
||||||
@ -205,7 +229,7 @@ impl PermissionManager {
|
|||||||
.filter(|perm| perm.resource_type == ResourceType::Db)
|
.filter(|perm| perm.resource_type == ResourceType::Db)
|
||||||
.filter(|perm| perm.action == action) // action ist nicht mehr Option
|
.filter(|perm| perm.action == action) // action ist nicht mehr Option
|
||||||
.any(|perm| {
|
.any(|perm| {
|
||||||
if perm.target != "*" && perm.target != table_name {
|
if perm.target != "*" && perm.target != clean_table_name {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
|||||||
@ -3,6 +3,8 @@ export default defineAppConfig({
|
|||||||
colors: {
|
colors: {
|
||||||
primary: 'sky',
|
primary: 'sky',
|
||||||
secondary: 'fuchsia',
|
secondary: 'fuchsia',
|
||||||
|
warning: 'yellow',
|
||||||
|
danger: 'red',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -14,18 +14,44 @@
|
|||||||
@apply cursor-not-allowed;
|
@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 {
|
#__nuxt {
|
||||||
/* Stellt sicher, dass die App immer die volle Höhe hat */
|
/* Volle Höhe des body */
|
||||||
min-height: 100vh;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
/* Sorgt dafür, dass Padding die Höhe nicht sprengt */
|
/* Safe-Area Paddings auf root element - damit ALLES davon profitiert */
|
||||||
@apply box-border;
|
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 */
|
box-sizing: border-box;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="desktopEl"
|
ref="desktopEl"
|
||||||
class="w-full h-full relative overflow-hidden"
|
class="absolute inset-0 overflow-hidden"
|
||||||
>
|
>
|
||||||
<Swiper
|
<Swiper
|
||||||
:modules="[SwiperNavigation]"
|
:modules="[SwiperNavigation]"
|
||||||
@ -13,7 +13,7 @@
|
|||||||
:no-swiping="true"
|
:no-swiping="true"
|
||||||
no-swiping-class="no-swipe"
|
no-swiping-class="no-swipe"
|
||||||
:allow-touch-move="allowSwipe"
|
:allow-touch-move="allowSwipe"
|
||||||
class="w-full h-full"
|
class="h-full w-full"
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
@swiper="onSwiperInit"
|
@swiper="onSwiperInit"
|
||||||
@slide-change="onSlideChange"
|
@slide-change="onSlideChange"
|
||||||
@ -115,6 +115,14 @@
|
|||||||
:source-height="window.sourceHeight"
|
:source-height="window.sourceHeight"
|
||||||
:is-opening="window.isOpening"
|
:is-opening="window.isOpening"
|
||||||
:is-closing="window.isClosing"
|
:is-closing="window.isClosing"
|
||||||
|
:warning-level="
|
||||||
|
window.type === 'extension' &&
|
||||||
|
availableExtensions.find(
|
||||||
|
(ext) => ext.id === window.sourceId,
|
||||||
|
)?.devServerUrl
|
||||||
|
? 'warning'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
class="no-swipe"
|
class="no-swipe"
|
||||||
@close="windowManager.closeWindow(window.id)"
|
@close="windowManager.closeWindow(window.id)"
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
@ -164,6 +172,13 @@
|
|||||||
:source-height="window.sourceHeight"
|
:source-height="window.sourceHeight"
|
||||||
:is-opening="window.isOpening"
|
:is-opening="window.isOpening"
|
||||||
:is-closing="window.isClosing"
|
:is-closing="window.isClosing"
|
||||||
|
:warning-level="
|
||||||
|
window.type === 'extension' &&
|
||||||
|
availableExtensions.find((ext) => ext.id === window.sourceId)
|
||||||
|
?.devServerUrl
|
||||||
|
? 'warning'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
class="no-swipe"
|
class="no-swipe"
|
||||||
@close="windowManager.closeWindow(window.id)"
|
@close="windowManager.closeWindow(window.id)"
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div
|
<div
|
||||||
v-if="preview?.manifest.icon"
|
v-if="preview?.manifest.icon"
|
||||||
class="w-16 h-16 flex-shrink-0"
|
class="w-16 h-16 shrink-0"
|
||||||
>
|
>
|
||||||
<UIcon
|
<UIcon
|
||||||
:name="preview.manifest.icon"
|
:name="preview.manifest.icon"
|
||||||
@ -184,7 +184,6 @@ const shellPermissions = computed({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const permissionAccordionItems = computed(() => {
|
const permissionAccordionItems = computed(() => {
|
||||||
const items = []
|
const items = []
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,17 @@
|
|||||||
ref="windowEl"
|
ref="windowEl"
|
||||||
:style="windowStyle"
|
:style="windowStyle"
|
||||||
:class="[
|
:class="[
|
||||||
'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden isolate',
|
'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden',
|
||||||
'border border-gray-200 dark:border-gray-700 transition-all ease-out duration-600 ',
|
'transition-all ease-out duration-600',
|
||||||
'flex flex-col @container',
|
'flex flex-col @container',
|
||||||
{ 'select-none': isResizingOrDragging },
|
{ 'select-none': isResizingOrDragging },
|
||||||
isActive ? 'z-20' : 'z-10',
|
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"
|
@mousedown="handleActivate"
|
||||||
>
|
>
|
||||||
@ -86,6 +92,7 @@ const props = defineProps<{
|
|||||||
sourceHeight?: number
|
sourceHeight?: number
|
||||||
isOpening?: boolean
|
isOpening?: boolean
|
||||||
isClosing?: boolean
|
isClosing?: boolean
|
||||||
|
warningLevel?: 'warning' | 'danger' // Warning indicator (e.g., dev extension, dangerous permissions)
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -315,13 +322,33 @@ const handleMaximize = () => {
|
|||||||
const bounds = getViewportBounds()
|
const bounds = getViewportBounds()
|
||||||
|
|
||||||
if (bounds && bounds.width > 0 && bounds.height > 0) {
|
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
|
x.value = 0
|
||||||
y.value = 0
|
y.value = 0 // Start below header and status bar
|
||||||
width.value = bounds.width
|
width.value = bounds.width
|
||||||
height.value = bounds.height
|
// Height: viewport - header - both safe-areas
|
||||||
|
height.value = bounds.height - headerHeight - safeAreaTop - safeAreaBottom
|
||||||
isMaximized.value = true
|
isMaximized.value = true
|
||||||
}
|
}
|
||||||
console.log('handleMaximize', preMaximizeState, bounds)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -368,7 +368,8 @@ async function handleDatabaseMethodAsync(
|
|||||||
const rows = await invoke<unknown[]>('extension_sql_select', {
|
const rows = await invoke<unknown[]>('extension_sql_select', {
|
||||||
sql: params.query || '',
|
sql: params.query || '',
|
||||||
params: params.params || [],
|
params: params.params || [],
|
||||||
extensionId: extension.id,
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -379,14 +380,15 @@ async function handleDatabaseMethodAsync(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'haextension.db.execute': {
|
case 'haextension.db.execute': {
|
||||||
await invoke<string[]>('extension_sql_execute', {
|
const rows = await invoke<unknown[]>('extension_sql_execute', {
|
||||||
sql: params.query || '',
|
sql: params.query || '',
|
||||||
params: params.params || [],
|
params: params.params || [],
|
||||||
extensionId: extension.id,
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: [],
|
rows,
|
||||||
rowsAffected: 1,
|
rowsAffected: 1,
|
||||||
lastInsertId: undefined,
|
lastInsertId: undefined,
|
||||||
}
|
}
|
||||||
@ -400,7 +402,8 @@ async function handleDatabaseMethodAsync(
|
|||||||
await invoke('extension_sql_execute', {
|
await invoke('extension_sql_execute', {
|
||||||
sql: stmt,
|
sql: stmt,
|
||||||
params: [],
|
params: [],
|
||||||
extensionId: extension.id,
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-dvw h-dvh flex flex-col">
|
<div class="w-full h-full flex flex-col">
|
||||||
<UPageHeader
|
<UPageHeader
|
||||||
|
ref="headerEl"
|
||||||
as="header"
|
as="header"
|
||||||
:ui="{
|
:ui="{
|
||||||
root: ['px-8 py-0'],
|
root: ['px-8 py-0'],
|
||||||
@ -53,7 +54,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</UPageHeader>
|
</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 />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -93,11 +94,9 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
class="mt-6"
|
class="mt-6"
|
||||||
@click="handleAddWorkspace"
|
@click="handleAddWorkspace"
|
||||||
|
icon="i-heroicons-plus"
|
||||||
|
:label="t('add')"
|
||||||
>
|
>
|
||||||
<template #leading>
|
|
||||||
<UIcon name="i-heroicons-plus" />
|
|
||||||
</template>
|
|
||||||
New Workspace
|
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -127,6 +126,15 @@ const handleAddWorkspace = async () => {
|
|||||||
workspaceStore.slideToWorkspace(workspace?.id)
|
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>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
@ -136,10 +144,12 @@ de:
|
|||||||
|
|
||||||
header:
|
header:
|
||||||
workspaces: Workspaces
|
workspaces: Workspaces
|
||||||
|
add: Workspace hinzufügen
|
||||||
en:
|
en:
|
||||||
search:
|
search:
|
||||||
label: Search
|
label: Search
|
||||||
|
|
||||||
header:
|
header:
|
||||||
workspaces: Workspaces
|
workspaces: Workspaces
|
||||||
|
add: Add Workspace
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full flex items-center justify-center">
|
<UDashboardPanel resizable>
|
||||||
<HaexDesktop />
|
<HaexDesktop />
|
||||||
</div>
|
</UDashboardPanel>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
|||||||
currentExtension.value.publicKey,
|
currentExtension.value.publicKey,
|
||||||
currentExtension.value.name,
|
currentExtension.value.name,
|
||||||
currentExtension.value.version,
|
currentExtension.value.version,
|
||||||
'index.html',
|
currentExtension.value.entry ?? 'index.html',
|
||||||
currentExtension.value.devServerUrl ?? undefined,
|
currentExtension.value.devServerUrl ?? undefined,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -69,10 +69,12 @@ export const useUiStore = defineStore('uiStore', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const viewportHeightWithoutHeader = ref(0)
|
const viewportHeightWithoutHeader = ref(0)
|
||||||
|
const headerHeight = ref(0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
availableThemes,
|
availableThemes,
|
||||||
viewportHeightWithoutHeader,
|
viewportHeightWithoutHeader,
|
||||||
|
headerHeight,
|
||||||
currentTheme,
|
currentTheme,
|
||||||
currentThemeName,
|
currentThemeName,
|
||||||
defaultTheme,
|
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