diff --git a/nuxt.config.ts b/nuxt.config.ts index ed6cb08..7fbc33a 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -80,6 +80,8 @@ export default defineNuxtConfig({ redirectOn: 'root', // recommended }, types: 'composition', + + vueI18n: './i18n.config.ts', }, zodI18n: { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 221919a..8c406ee 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1696,6 +1696,7 @@ dependencies = [ "ed25519-dalek", "fs_extra", "hex", + "lazy_static", "mime", "mime_guess", "rusqlite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6575f83..66d357f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,6 +32,7 @@ base64 = "0.22" ed25519-dalek = "2.1" fs_extra = "1.3.0" hex = "0.4" +lazy_static = "1.5" mime = "0.3" mime_guess = "2.0" serde = { version = "1", features = ["derive"] } diff --git a/src-tauri/bindings/DbAction.ts b/src-tauri/bindings/DbAction.ts index 4782511..449958b 100644 --- a/src-tauri/bindings/DbAction.ts +++ b/src-tauri/bindings/DbAction.ts @@ -3,4 +3,4 @@ /** * Definiert Aktionen, die auf eine Datenbank angewendet werden können. */ -export type DbAction = "read" | "readwrite" | "create" | "delete" | "alterdrop"; +export type DbAction = "read" | "readWrite" | "create" | "delete" | "alterDrop"; diff --git a/src-tauri/bindings/ExtensionInfoResponse.ts b/src-tauri/bindings/ExtensionInfoResponse.ts index c794c02..e93574b 100644 --- a/src-tauri/bindings/ExtensionInfoResponse.ts +++ b/src-tauri/bindings/ExtensionInfoResponse.ts @@ -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 = { key_hash: string, name: string, full_id: string, version: string, display_name: string | null, namespace: string | null, allowed_origin: string, }; +export type ExtensionInfoResponse = { keyHash: string, name: string, fullId: string, version: string, displayName: string | null, namespace: string | null, allowedOrigin: string, }; diff --git a/src-tauri/bindings/FsAction.ts b/src-tauri/bindings/FsAction.ts index b42ac58..9529d0c 100644 --- a/src-tauri/bindings/FsAction.ts +++ b/src-tauri/bindings/FsAction.ts @@ -3,4 +3,4 @@ /** * Definiert Aktionen, die auf das Dateisystem angewendet werden können. */ -export type FsAction = "read" | "readwrite"; +export type FsAction = "read" | "readWrite"; diff --git a/src-tauri/database/vault.db b/src-tauri/database/vault.db index 5247ff4..9d40fc3 100644 Binary files a/src-tauri/database/vault.db and b/src-tauri/database/vault.db differ diff --git a/src-tauri/gen/android/app/src/main/assets/database/vault.db b/src-tauri/gen/android/app/src/main/assets/database/vault.db index 3c09876..9d40fc3 100644 Binary files a/src-tauri/gen/android/app/src/main/assets/database/vault.db and b/src-tauri/gen/android/app/src/main/assets/database/vault.db differ diff --git a/src-tauri/gen/schemas/android-schema.json b/src-tauri/gen/schemas/android-schema.json index 1d9a470..b3824e5 100644 --- a/src-tauri/gen/schemas/android-schema.json +++ b/src-tauri/gen/schemas/android-schema.json @@ -2270,12 +2270,6 @@ "Identifier": { "description": "Permission identifier", "oneOf": [ - { - "description": "Default permissions for the plugin", - "type": "string", - "const": "android-fs:default", - "markdownDescription": "Default permissions for the plugin" - }, { "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", "type": "string", diff --git a/src-tauri/gen/schemas/mobile-schema.json b/src-tauri/gen/schemas/mobile-schema.json index 1d9a470..b3824e5 100644 --- a/src-tauri/gen/schemas/mobile-schema.json +++ b/src-tauri/gen/schemas/mobile-schema.json @@ -2270,12 +2270,6 @@ "Identifier": { "description": "Permission identifier", "oneOf": [ - { - "description": "Default permissions for the plugin", - "type": "string", - "const": "android-fs:default", - "markdownDescription": "Default permissions for the plugin" - }, { "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", "type": "string", diff --git a/src-tauri/src/crdt/transformer.rs b/src-tauri/src/crdt/transformer.rs index f790ef9..9d148b5 100644 --- a/src-tauri/src/crdt/transformer.rs +++ b/src-tauri/src/crdt/transformer.rs @@ -687,23 +687,36 @@ impl CrdtTransformer { insert_stmt: &mut Insert, timestamp: &Timestamp, ) -> Result<(), DatabaseError> { + // Add both haex_timestamp and haex_tombstone columns insert_stmt .columns .push(Ident::new(self.columns.hlc_timestamp)); + insert_stmt + .columns + .push(Ident::new(self.columns.tombstone)); match insert_stmt.source.as_mut() { Some(query) => match &mut *query.body { SetExpr::Values(values) => { for row in &mut values.rows { + // Add haex_timestamp value row.push(Expr::Value( Value::SingleQuotedString(timestamp.to_string()).into(), )); + // Add haex_tombstone value (0 = not deleted) + row.push(Expr::Value( + Value::Number("0".to_string(), false).into(), + )); } } SetExpr::Select(select) => { let hlc_expr = Expr::Value(Value::SingleQuotedString(timestamp.to_string()).into()); select.projection.push(SelectItem::UnnamedExpr(hlc_expr)); + // Add haex_tombstone value (0 = not deleted) + let tombstone_expr = + Expr::Value(Value::Number("0".to_string(), false).into()); + select.projection.push(SelectItem::UnnamedExpr(tombstone_expr)); } _ => { return Err(DatabaseError::UnsupportedStatement { diff --git a/src-tauri/src/extension/core/manager.rs b/src-tauri/src/extension/core/manager.rs index 183d3d0..8766394 100644 --- a/src-tauri/src/extension/core/manager.rs +++ b/src-tauri/src/extension/core/manager.rs @@ -8,10 +8,11 @@ use crate::extension::database::executor::SqlExecutor; use crate::extension::error::ExtensionError; use crate::extension::permissions::manager::PermissionManager; use crate::extension::permissions::types::ExtensionPermission; -use crate::table_names::TABLE_EXTENSIONS; +use crate::table_names::{TABLE_EXTENSIONS, TABLE_EXTENSION_PERMISSIONS}; use crate::AppState; use std::collections::HashMap; -use std::fs::{self, File}; +use std::fs; +use std::io::Cursor; use std::path::PathBuf; use std::sync::Mutex; use std::time::{Duration, SystemTime}; @@ -33,6 +34,7 @@ pub struct MissingExtension { } struct ExtensionDataFromDb { + full_extension_id: String, manifest: ExtensionManifest, enabled: bool, } @@ -64,19 +66,19 @@ impl ExtensionManager { /// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest fn extract_and_validate_extension( - source_path: &str, + bytes: Vec, temp_prefix: &str, ) -> Result { - let source = PathBuf::from(source_path); let temp = std::env::temp_dir().join(format!("{}_{}", temp_prefix, uuid::Uuid::new_v4())); - std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?; + fs::create_dir_all(&temp) + .map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?; - let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?; - let mut archive = - ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed { + let mut archive = ZipArchive::new(Cursor::new(bytes)).map_err(|e| { + ExtensionError::InstallationFailed { reason: format!("Invalid ZIP: {}", e), - })?; + } + })?; archive .extract(&temp) @@ -84,7 +86,30 @@ impl ExtensionManager { reason: format!("Cannot extract ZIP: {}", e), })?; + // Check if manifest.json is directly in temp or in a subdirectory let manifest_path = temp.join("manifest.json"); + let actual_dir = if manifest_path.exists() { + temp.clone() + } else { + // manifest.json is in a subdirectory - find it + let mut found_dir = None; + for entry in fs::read_dir(&temp) + .map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))? + { + let entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?; + let path = entry.path(); + if path.is_dir() && path.join("manifest.json").exists() { + found_dir = Some(path); + break; + } + } + + found_dir.ok_or_else(|| ExtensionError::ManifestError { + reason: "manifest.json not found in extension archive".to_string(), + })? + }; + + let manifest_path = actual_dir.join("manifest.json"); let manifest_content = std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError { reason: format!("Cannot read manifest: {}", e), @@ -92,14 +117,14 @@ impl ExtensionManager { let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?; - let content_hash = ExtensionCrypto::hash_directory(&temp).map_err(|e| { + let content_hash = ExtensionCrypto::hash_directory(&actual_dir).map_err(|e| { ExtensionError::SignatureVerificationFailed { reason: e.to_string(), } })?; Ok(ExtractedExtension { - temp_dir: temp, + temp_dir: actual_dir, manifest, content_hash, }) @@ -119,7 +144,8 @@ impl ExtensionManager { // Sicherstellen, dass das Basisverzeichnis existiert if !path.exists() { - fs::create_dir_all(&path).map_err(|e| ExtensionError::Filesystem { source: e })?; + fs::create_dir_all(&path) + .map_err(|e| ExtensionError::filesystem_with_path(path.display().to_string(), e))?; } Ok(path) } @@ -145,9 +171,34 @@ impl ExtensionManager { app_handle: &AppHandle, full_extension_id: &str, ) -> Result { + // Parse full_extension_id: key_hash_name_version + // Split on first underscore to get key_hash + let first_underscore = + full_extension_id + .find('_') + .ok_or_else(|| ExtensionError::ValidationError { + reason: format!("Invalid full_extension_id format: {}", full_extension_id), + })?; + + let key_hash = &full_extension_id[..first_underscore]; + let rest = &full_extension_id[first_underscore + 1..]; + + // Split on last underscore to get version + let last_underscore = rest + .rfind('_') + .ok_or_else(|| ExtensionError::ValidationError { + reason: format!("Invalid full_extension_id format: {}", full_extension_id), + })?; + + let name = &rest[..last_underscore]; + let version = &rest[last_underscore + 1..]; + + // Build hierarchical path: key_hash/name/version/ let specific_extension_dir = self .get_base_extension_dir(app_handle)? - .join(full_extension_id); + .join(key_hash) + .join(name) + .join(version); Ok(specific_extension_dir) } @@ -220,14 +271,44 @@ impl ExtensionManager { }) } + pub async fn remove_extension_by_full_id( + &self, + app_handle: &AppHandle, + full_extension_id: &str, + state: &State<'_, AppState>, + ) -> Result<(), ExtensionError> { + // Parse full_extension_id: key_hash_name_version + // Since _ is not allowed in name and version, we can split safely + let parts: Vec<&str> = full_extension_id.split('_').collect(); + + if parts.len() != 3 { + return Err(ExtensionError::ValidationError { + reason: format!( + "Invalid full_extension_id format (expected 3 parts): {}", + full_extension_id + ), + }); + } + + let key_hash = parts[0]; + let name = parts[1]; + let version = parts[2]; + + self.remove_extension_internal(app_handle, key_hash, name, version, state) + .await + } + pub async fn remove_extension_internal( &self, app_handle: &AppHandle, key_hash: &str, - extension_id: &str, + extension_name: &str, extension_version: &str, state: &State<'_, AppState>, ) -> Result<(), ExtensionError> { + // Erstelle full_extension_id: key_hash_name_version + let full_extension_id = format!("{}_{}_{}",key_hash, extension_name, extension_version); + // Lösche Permissions und Extension-Eintrag in einer Transaktion with_connection(&state.db, |conn| { let tx = conn.transaction().map_err(DatabaseError::from)?; @@ -236,31 +317,59 @@ impl ExtensionManager { reason: "Failed to lock HLC service".to_string(), })?; - // Lösche alle Permissions - PermissionManager::delete_permissions_in_transaction(&tx, &hlc_service, extension_id)?; + // Lösche alle Permissions mit full_extension_id + PermissionManager::delete_permissions_in_transaction( + &tx, + &hlc_service, + &full_extension_id, + )?; - // Lösche Extension-Eintrag + // Lösche Extension-Eintrag mit full_extension_id let sql = format!("DELETE FROM {} WHERE id = ?", TABLE_EXTENSIONS); SqlExecutor::execute_internal_typed( &tx, &hlc_service, &sql, - rusqlite::params![extension_id], + rusqlite::params![full_extension_id], )?; tx.commit().map_err(DatabaseError::from) })?; - // Entferne aus dem In-Memory-Manager - self.remove_extension(&extension_id)?; + // Entferne aus dem In-Memory-Manager mit full_extension_id + self.remove_extension(&full_extension_id)?; - // Lösche Dateien vom Dateisystem + // Lösche nur den spezifischen Versions-Ordner: key_hash/name/version let extension_dir = - self.get_extension_dir(app_handle, key_hash, extension_id, extension_version)?; + self.get_extension_dir(app_handle, key_hash, extension_name, extension_version)?; if extension_dir.exists() { - std::fs::remove_dir_all(&extension_dir) - .map_err(|e| ExtensionError::Filesystem { source: e })?; + std::fs::remove_dir_all(&extension_dir).map_err(|e| { + ExtensionError::filesystem_with_path(extension_dir.display().to_string(), e) + })?; + + // Versuche, leere Parent-Ordner zu löschen + // 1. Extension-Name-Ordner (key_hash/name) + if let Some(name_dir) = extension_dir.parent() { + if name_dir.exists() { + if let Ok(entries) = std::fs::read_dir(name_dir) { + if entries.count() == 0 { + let _ = std::fs::remove_dir(name_dir); + + // 2. Key-Hash-Ordner (key_hash) - nur wenn auch leer + if let Some(key_hash_dir) = name_dir.parent() { + if key_hash_dir.exists() { + if let Ok(entries) = std::fs::read_dir(key_hash_dir) { + if entries.count() == 0 { + let _ = std::fs::remove_dir(key_hash_dir); + } + } + } + } + } + } + } + } } Ok(()) @@ -268,9 +377,9 @@ impl ExtensionManager { pub async fn preview_extension_internal( &self, - source_path: String, + file_bytes: Vec, ) -> Result { - let extracted = Self::extract_and_validate_extension(&source_path, "haexhub_preview")?; + let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview")?; let is_valid_signature = ExtensionCrypto::verify_signature( &extracted.manifest.public_key, @@ -293,11 +402,11 @@ impl ExtensionManager { pub async fn install_extension_with_permissions_internal( &self, app_handle: AppHandle, - source_path: String, + file_bytes: Vec, custom_permissions: EditablePermissions, state: &State<'_, AppState>, ) -> Result { - let extracted = Self::extract_and_validate_extension(&source_path, "haexhub_ext")?; + let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext")?; // Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft) ExtensionCrypto::verify_signature( @@ -316,17 +425,95 @@ impl ExtensionManager { &extracted.manifest.version, )?; - std::fs::create_dir_all(&extensions_dir) - .map_err(|e| ExtensionError::Filesystem { source: e })?; + std::fs::create_dir_all(&extensions_dir).map_err(|e| { + ExtensionError::filesystem_with_path(extensions_dir.display().to_string(), e) + })?; - copy_directory( - extracted.temp_dir.to_string_lossy().to_string(), - extensions_dir.to_string_lossy().to_string(), - )?; + // Copy contents of extracted.temp_dir to extensions_dir + // Note: extracted.temp_dir already points to the correct directory with manifest.json + for entry in fs::read_dir(&extracted.temp_dir).map_err(|e| { + ExtensionError::filesystem_with_path(extracted.temp_dir.display().to_string(), e) + })? { + let entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?; + let path = entry.path(); + let file_name = entry.file_name(); + let dest_path = extensions_dir.join(&file_name); + + if path.is_dir() { + copy_directory( + path.to_string_lossy().to_string(), + dest_path.to_string_lossy().to_string(), + )?; + } else { + fs::copy(&path, &dest_path).map_err(|e| { + ExtensionError::filesystem_with_path(path.display().to_string(), e) + })?; + } + } let permissions = custom_permissions.to_internal_permissions(&full_extension_id); - PermissionManager::save_permissions(state, &permissions).await?; + // Extension-Eintrag und Permissions in einer Transaktion speichern + with_connection(&state.db, |conn| { + let tx = conn.transaction().map_err(DatabaseError::from)?; + + let hlc_service = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned { + reason: "Failed to lock HLC service".to_string(), + })?; + + // 1. Extension-Eintrag erstellen (oder aktualisieren falls schon vorhanden) + let insert_ext_sql = format!( + "INSERT OR REPLACE INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + TABLE_EXTENSIONS + ); + + SqlExecutor::execute_internal_typed( + &tx, + &hlc_service, + &insert_ext_sql, + rusqlite::params![ + full_extension_id, + extracted.manifest.name, + extracted.manifest.version, + extracted.manifest.author, + extracted.manifest.entry, + extracted.manifest.icon, + extracted.manifest.public_key, + extracted.manifest.signature, + extracted.manifest.homepage, + extracted.manifest.description, + true, // enabled + ], + )?; + + // 2. Permissions speichern (oder aktualisieren falls schon vorhanden) + let insert_perm_sql = format!( + "INSERT OR REPLACE INTO {} (id, extension_id, resource_type, action, target, constraints, status) VALUES (?, ?, ?, ?, ?, ?, ?)", + TABLE_EXTENSION_PERMISSIONS + ); + + for perm in &permissions { + use crate::database::generated::HaexExtensionPermissions; + let db_perm: HaexExtensionPermissions = perm.into(); + + SqlExecutor::execute_internal_typed( + &tx, + &hlc_service, + &insert_perm_sql, + rusqlite::params![ + db_perm.id, + db_perm.extension_id, + db_perm.resource_type, + db_perm.action, + db_perm.target, + db_perm.constraints, + db_perm.status, + ], + )?; + } + + tx.commit().map_err(DatabaseError::from) + })?; let extension = Extension { id: full_extension_id.clone(), @@ -372,16 +559,28 @@ impl ExtensionManager { // Schritt 1: Alle Daten aus der Datenbank in einem Rutsch laden. let extensions = with_connection(&state.db, |conn| { - let sql = "SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled FROM haexExtensions"; - let results = SqlExecutor::select_internal(conn, sql, &[])?; + let sql = format!( + "SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled FROM {}", + TABLE_EXTENSIONS + ); + eprintln!("DEBUG: SQL Query before transformation: {}", sql); + let results = SqlExecutor::select_internal(conn, &sql, &[])?; + eprintln!("DEBUG: Query returned {} results", results.len()); let mut data = Vec::new(); for result in results { + let full_extension_id = result["id"] + .as_str() + .ok_or_else(|| DatabaseError::SerializationError { + reason: "Missing id field".to_string(), + })? + .to_string(); + let manifest = ExtensionManifest { - id: result["id"] + id: result["name"] .as_str() .ok_or_else(|| DatabaseError::SerializationError { - reason: "Missing id field".to_string(), + reason: "Missing name field".to_string(), })? .to_string(), name: result["name"] @@ -411,7 +610,11 @@ impl ExtensionManager { .or_else(|| result["enabled"].as_i64().map(|v| v != 0)) .unwrap_or(false); - data.push(ExtensionDataFromDb { manifest, enabled }); + data.push(ExtensionDataFromDb { + full_extension_id, + manifest, + enabled, + }); } Ok(data) })?; @@ -419,12 +622,19 @@ impl ExtensionManager { // Schritt 2: Die gesammelten Daten verarbeiten (Dateisystem, State-Mutationen). let mut loaded_extension_ids = Vec::new(); - for extension in extensions { - let full_extension_id = extension.manifest.full_extension_id()?; + eprintln!("DEBUG: Found {} extensions in database", extensions.len()); + + for extension_data in extensions { + let full_extension_id = extension_data.full_extension_id; + eprintln!("DEBUG: Processing extension: {}", full_extension_id); let extension_path = self.get_extension_path_by_full_extension_id(app_handle, &full_extension_id)?; if !extension_path.exists() || !extension_path.join("manifest.json").exists() { + eprintln!( + "DEBUG: Extension files missing for: {} at {:?}", + full_extension_id, extension_path + ); self.missing_extensions .lock() .map_err(|e| ExtensionError::MutexPoisoned { @@ -432,26 +642,31 @@ impl ExtensionManager { })? .push(MissingExtension { full_extension_id: full_extension_id.clone(), - name: extension.manifest.name.clone(), - version: extension.manifest.version.clone(), + name: extension_data.manifest.name.clone(), + version: extension_data.manifest.version.clone(), }); continue; } + eprintln!( + "DEBUG: Extension loaded successfully: {}", + full_extension_id + ); + let extension = Extension { id: full_extension_id.clone(), - name: extension.manifest.name.clone(), + name: extension_data.manifest.name.clone(), source: ExtensionSource::Production { path: extension_path, - version: extension.manifest.version.clone(), + version: extension_data.manifest.version.clone(), }, - manifest: extension.manifest, - enabled: extension.enabled, + manifest: extension_data.manifest, + enabled: extension_data.enabled, last_accessed: SystemTime::now(), }; + loaded_extension_ids.push(full_extension_id.clone()); self.add_production_extension(extension)?; - loaded_extension_ids.push(full_extension_id); } Ok(loaded_extension_ids) diff --git a/src-tauri/src/extension/core/manifest.rs b/src-tauri/src/extension/core/manifest.rs index 1b1b0fd..eec7aa7 100644 --- a/src-tauri/src/extension/core/manifest.rs +++ b/src-tauri/src/extension/core/manifest.rs @@ -76,6 +76,18 @@ impl ExtensionManifest { } pub fn full_extension_id(&self) -> Result { + // Validate that name and version don't contain underscores + if self.name.contains('_') { + return Err(ExtensionError::ValidationError { + reason: format!("Extension name cannot contain underscores: {}", self.name), + }); + } + if self.version.contains('_') { + return Err(ExtensionError::ValidationError { + reason: format!("Extension version cannot contain underscores: {}", self.version), + }); + } + let key_hash = self.calculate_key_hash()?; Ok(format!("{}_{}_{}", key_hash, self.name, self.version)) } @@ -175,6 +187,7 @@ impl ExtensionPermissions { #[derive(Serialize, Deserialize, Clone, Debug, TS)] #[ts(export)] +#[serde(rename_all = "camelCase")] pub struct ExtensionInfoResponse { pub key_hash: String, pub name: String, @@ -189,10 +202,15 @@ impl ExtensionInfoResponse { pub fn from_extension( extension: &crate::extension::core::types::Extension, ) -> Result { - // Annahme: get_tauri_origin ist in deinem `types`-Modul oder woanders definiert use crate::extension::core::types::get_tauri_origin; + // In development mode, use a wildcard for localhost to match any port + #[cfg(debug_assertions)] + let allowed_origin = "http://localhost:3003".to_string(); + + #[cfg(not(debug_assertions))] let allowed_origin = get_tauri_origin(); + let key_hash = extension.manifest.calculate_key_hash()?; let full_id = extension.manifest.full_extension_id()?; diff --git a/src-tauri/src/extension/core/protocol.rs b/src-tauri/src/extension/core/protocol.rs index 41c26fd..bc78bdf 100644 --- a/src-tauri/src/extension/core/protocol.rs +++ b/src-tauri/src/extension/core/protocol.rs @@ -1,16 +1,27 @@ // src-tauri/src/extension/core/protocol.rs +use crate::extension::core::types::get_tauri_origin; use crate::extension::error::ExtensionError; use crate::AppState; use mime; use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; use std::fmt; use std::fs; use std::path::PathBuf; +use std::sync::Mutex; +use tauri::http::Uri; use tauri::http::{Request, Response}; use tauri::{AppHandle, State}; -#[derive(Deserialize, Debug)] +// Cache for modified HTML files (extension_id -> modified content) +lazy_static::lazy_static! { + static ref HTML_CACHE: Mutex>> = Mutex::new(HashMap::new()); + static ref EXTENSION_CACHE: Mutex> = Mutex::new(None); +} + +#[derive(Deserialize, Serialize, Debug, Clone)] struct ExtensionInfo { key_hash: String, name: String, @@ -22,6 +33,7 @@ enum DataProcessingError { HexDecoding(hex::FromHexError), Utf8Conversion(std::string::FromUtf8Error), JsonParsing(serde_json::Error), + Custom(String), } impl fmt::Display for DataProcessingError { @@ -32,6 +44,7 @@ impl fmt::Display for DataProcessingError { write!(f, "UTF-8-Konvertierungsfehler: {}", e) } DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {}", e), + DataProcessingError::Custom(msg) => write!(f, "Datenverarbeitungsfehler: {}", msg), } } } @@ -42,10 +55,17 @@ impl std::error::Error for DataProcessingError { DataProcessingError::HexDecoding(e) => Some(e), DataProcessingError::Utf8Conversion(e) => Some(e), DataProcessingError::JsonParsing(e) => Some(e), + DataProcessingError::Custom(_) => None, } } } +impl From for DataProcessingError { + fn from(msg: String) -> Self { + DataProcessingError::Custom(msg) + } +} + impl From for DataProcessingError { fn from(err: hex::FromHexError) -> Self { DataProcessingError::HexDecoding(err) @@ -152,101 +172,770 @@ pub fn extension_protocol_handler( app_handle: &AppHandle, request: &Request>, ) -> Result>, Box> { - let uri_ref = request.uri(); - println!("Protokoll Handler für: {}", uri_ref); + // Get the origin from the request + let origin = request + .headers() + .get("origin") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); - let host = uri_ref - .host() - .ok_or("Kein Host (Extension ID) in URI gefunden")? - .to_string(); + // Only allow same-protocol requests (haex-extension://) or tauri origin + // For null/empty origin (initial load), use wildcard + let allowed_origin = if origin.starts_with("haex-extension://") || origin == get_tauri_origin() + { + origin + } else if origin.is_empty() || origin == "null" { + "*" // Allow initial load without origin + } else { + // Reject other origins + return Response::builder() + .status(403) + .body(Vec::from("Origin not allowed")) + .map_err(|e| e.into()); + }; + + // Handle OPTIONS requests for CORS preflight + if request.method() == "OPTIONS" { + return Response::builder() + .status(200) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .header("Access-Control-Allow-Credentials", "true") + .body(Vec::new()) + .map_err(|e| e.into()); + } + + let uri_ref = request.uri(); + let referer = request + .headers() + .get("referer") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + println!("Protokoll Handler für: {}", uri_ref); + println!("Origin: {}", origin); + println!("Referer: {}", referer); + + /* let encoded_info = + match parse_encoded_info_from_origin_or_uri_or_referer(&origin, uri_ref, &referer) { + Ok(info) => info, + Err(e) => { + eprintln!("Fehler beim Parsen des Origin für Extension-Info: {}", e); + return Response::builder() + .status(400) + .header("Access-Control-Allow-Origin", allowed_origin) + .body(Vec::from(format!("Ungültiger Origin: {}", e))) + .map_err(|e| e.into()); + } + }; */ + + let info = + match parse_encoded_info_from_origin_or_uri_or_referer_or_cache(&origin, uri_ref, &referer) + { + Ok(decoded) => { + println!("=== Extension Protocol Handler ==="); + println!("Full URI: {}", uri_ref); + println!( + "Encoded Info (aus Origin/URI/Referer/Cache): {}", + encode_hex_for_log(&decoded) + ); // Hilfs-Log + println!("Decoded info:"); + println!(" KeyHash: {}", decoded.key_hash); + println!(" Name: {}", decoded.name); + println!(" Version: {}", decoded.version); + decoded + } + Err(e) => { + eprintln!("Fehler beim Parsen (alle Fallbacks): {}", e); + return Response::builder() + .status(400) + .header("Access-Control-Allow-Origin", allowed_origin) + .body(Vec::from(format!("Ungültige Anfrage: {}", e))) + .map_err(|e| e.into()); + } + }; let path_str = uri_ref.path(); let segments_iter = path_str.split('/').filter(|s| !s.is_empty()); let resource_segments: Vec<&str> = segments_iter.collect(); let raw_asset_path = resource_segments.join("/"); + + // Handle SPA routing - serve index.html for all non-asset paths let asset_to_load = if raw_asset_path.is_empty() { "index.html" - } else { + } else if raw_asset_path.starts_with("_nuxt/") + || raw_asset_path.ends_with(".js") + || raw_asset_path.ends_with(".css") + || raw_asset_path.ends_with(".json") + || raw_asset_path.ends_with(".ico") + || raw_asset_path.ends_with(".txt") + || raw_asset_path.ends_with(".svg") + || raw_asset_path.ends_with(".png") + || raw_asset_path.ends_with(".jpg") + || raw_asset_path.ends_with(".jpeg") + || raw_asset_path.ends_with(".gif") + || raw_asset_path.ends_with(".woff") + || raw_asset_path.ends_with(".woff2") + || raw_asset_path.ends_with(".ttf") + || raw_asset_path.ends_with(".eot") + { + // Serve actual asset &raw_asset_path + } else { + // SPA fallback - serve index.html for routes + "index.html" }; - match process_hex_encoded_json(&host) { - Ok(info) => { - println!("Daten erfolgreich verarbeitet:"); - println!(" KeyHash: {}", info.key_hash); - println!(" Name: {}", info.name); - println!(" Version: {}", info.version); - let absolute_secure_path = resolve_secure_extension_asset_path( - app_handle, - state, - &info.key_hash, - &info.name, - &info.version, - &asset_to_load, - )?; + println!("Path: {}", path_str); + println!("Asset to load: {}", asset_to_load); - println!("absolute_secure_path: {}", absolute_secure_path.display()); + /* match process_hex_encoded_json(&encoded_info) { + Ok(info) => { + println!("=== Extension Protocol Handler ==="); + println!("Full URI: {}", uri_ref); + println!("Origin: {}", origin); + println!("Encoded Info (aus Origin): {}", encoded_info); + println!("Path: {}", path_str); + println!("Asset to load: {}", asset_to_load); + println!("Decoded info:"); + println!(" KeyHash: {}", info.key_hash); + println!(" Name: {}", info.name); + println!(" Version: {}", info.version); - if absolute_secure_path.exists() && absolute_secure_path.is_file() { - match fs::read(&absolute_secure_path) { - Ok(content) => { - let mime_type = mime_guess::from_path(&absolute_secure_path) - .first_or(mime::APPLICATION_OCTET_STREAM) - .to_string(); - let content_length = content.len(); - println!( - "Liefere {} ({}, {} bytes) ", - absolute_secure_path.display(), - mime_type, - content_length - ); - Response::builder() - .status(200) - .header("Content-Type", mime_type) - .header("Content-Length", content_length.to_string()) - .header("Accept-Ranges", "bytes") - .body(content) - .map_err(|e| e.into()) + let absolute_secure_path = resolve_secure_extension_asset_path( + app_handle, + state, + &info.key_hash, + &info.name, + &info.version, + &asset_to_load, + )?; + + println!("Resolved path: {}", absolute_secure_path.display()); + println!("File exists: {}", absolute_secure_path.exists()); + + if absolute_secure_path.exists() && absolute_secure_path.is_file() { + match fs::read(&absolute_secure_path) { + Ok(mut content) => { + let mime_type = mime_guess::from_path(&absolute_secure_path) + .first_or(mime::APPLICATION_OCTET_STREAM) + .to_string(); + + if asset_to_load == "index.html" && mime_type.contains("html") { + if let Ok(html_str) = String::from_utf8(content.clone()) { + let base_tag = format!(r#""#, encoded_info); + let modified_html = if let Some(head_pos) = html_str.find("") + { + let insert_pos = head_pos + 6; + format!( + "{}{}{}", + &html_str[..insert_pos], + base_tag, + &html_str[insert_pos..] + ) + } else { + // Fallback: Prepend + format!("{}{}", base_tag, html_str) + }; + content = modified_html.into_bytes(); + } + } + // Inject localStorage polyfill for HTML files with caching + if asset_to_load == "index.html" && mime_type.contains("html") { + // Create cache key: extension_id (from host) + let cache_key = format!("{}_{}", host, asset_to_load); + + // Check cache first + if let Ok(cache) = HTML_CACHE.lock() { + if let Some(cached_content) = cache.get(&cache_key) { + println!("Serving cached HTML for: {}", cache_key); + content = cached_content.clone(); + + let content_length = content.len(); + return Response::builder() + .status(200) + .header("Content-Type", mime_type) + .header("Content-Length", content_length.to_string()) + .header("Accept-Ranges", "bytes") + .header("X-HaexHub-Cache", "HIT") + .header("Access-Control-Allow-Origin", allowed_origin) + .header( + "Access-Control-Allow-Methods", + "GET, POST, OPTIONS", + ) + .header("Access-Control-Allow-Headers", "*") + .header("Access-Control-Allow-Credentials", "true") + .body(content) + .map_err(|e| e.into()); + } + } + + // Not in cache, modify and cache it + if let Ok(html_content) = String::from_utf8(content.clone()) { + let polyfill = r#""#; + + // Inject as the FIRST thing in , before any other script + let modified_html = if let Some(head_pos) = + html_content.find("") + { + let insert_pos = head_pos + 6; // After + format!( + "{}{}{}", + &html_content[..insert_pos], + polyfill, + &html_content[insert_pos..] + ) + } else if let Some(charset_pos) = html_content.find("{}{}", + &html_content[..charset_pos], + polyfill, + &html_content[charset_pos..] + ) + } else if let Some(html_pos) = html_content.find("") + { + // Insert right after DOCTYPE + let insert_pos = html_pos + 15; // After + format!( + "{}{}{}", + &html_content[..insert_pos], + polyfill, + &html_content[insert_pos..] + ) + } else { + // Prepend to entire file + format!("{}{}", polyfill, html_content) + }; + + content = modified_html.into_bytes(); + + // Cache the modified HTML + if let Ok(mut cache) = HTML_CACHE.lock() { + cache.insert(cache_key, content.clone()); + println!("Cached modified HTML for future requests"); + } + } + } + + let content_length = content.len(); + println!( + "Liefere {} ({}, {} bytes) ", + absolute_secure_path.display(), + mime_type, + content_length + ); + Response::builder() + .status(200) + .header("Content-Type", &mime_type) + .header("Content-Length", content_length.to_string()) + .header("Accept-Ranges", "bytes") + .header( + "X-HaexHub-Cache", + if asset_to_load == "index.html" && mime_type.contains("html") { + "MISS" + } else { + "N/A" + }, + ) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .header("Access-Control-Allow-Credentials", "true") + .body(content) + .map_err(|e| e.into()) + } + Err(e) => { + eprintln!( + "Fehler beim Lesen der Datei {}: {}", + absolute_secure_path.display(), + e + ); + let status_code = if e.kind() == std::io::ErrorKind::NotFound { + 404 + } else if e.kind() == std::io::ErrorKind::PermissionDenied { + 403 + } else { + 500 + }; + + Response::builder() + .status(status_code) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .body(Vec::new()) + .map_err(|e| e.into()) + } + } + } else { + eprintln!( + "Asset nicht gefunden oder ist kein File: {}", + absolute_secure_path.display() + ); + Response::builder() + .status(404) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .body(Vec::new()) + .map_err(|e| e.into()) + } + } + Err(e) => { + eprintln!("Fehler bei der Datenverarbeitung: {}", e); + + Response::builder() + .status(500) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .body(Vec::new()) + .map_err(|e| e.into()) + } + } + */ + + let absolute_secure_path = resolve_secure_extension_asset_path( + app_handle, + state, + &info.key_hash, + &info.name, + &info.version, + &asset_to_load, + )?; + + println!("Resolved path: {}", absolute_secure_path.display()); + println!("File exists: {}", absolute_secure_path.exists()); + + if absolute_secure_path.exists() && absolute_secure_path.is_file() { + match fs::read(&absolute_secure_path) { + Ok(mut content) => { + let mime_type = mime_guess::from_path(&absolute_secure_path) + .first_or(mime::APPLICATION_OCTET_STREAM) + .to_string(); + + // Für index.html – injiziere Tag + localStorage-Polyfill + if asset_to_load == "index.html" && mime_type.contains("html") { + // Cache-Key erstellen (extension-host + asset) + let host = uri_ref + .host() + .map_or("unknown".to_string(), |h| h.to_string()); + let cache_key = format!("{}_{}", host, asset_to_load); + + // Cache checken (aus deinem alten Code) + if let Ok(cache_guard) = HTML_CACHE.lock() { + if let Some(cached_content) = cache_guard.get(&cache_key) { + println!("Serving cached HTML for: {}", cache_key); + let content_length = cached_content.len(); + return Response::builder() + .status(200) + .header("Content-Type", &mime_type) + .header("Content-Length", content_length.to_string()) + .header("Accept-Ranges", "bytes") + .header("X-HaexHub-Cache", "HIT") + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .header("Access-Control-Allow-Credentials", "true") + .body(cached_content.clone()) + .map_err(|e| e.into()); + } } - Err(e) => { - eprintln!( - "Fehler beim Lesen der Datei {}: {}", - absolute_secure_path.display(), - e - ); - let status_code = if e.kind() == std::io::ErrorKind::NotFound { - 404 - } else if e.kind() == std::io::ErrorKind::PermissionDenied { - 403 + + // Nicht gecacht: Modifiziere HTML + if let Ok(html_str) = String::from_utf8(content.clone()) { + // 1. Polyfill injizieren (als ERSTES in ) + let polyfill_script = r#""#; + + // 2. Base-Tag erstellen + let base_tag = format!(r#""#, encode_hex_for_log(&info)); + + // 3. Beide in injizieren: Polyfill zuerst, dann Base-Tag + let modified_html = if let Some(head_pos) = html_str.find("") { + let insert_pos = head_pos + 6; // Nach + format!( + "{}{}{}{}", + &html_str[..insert_pos], + polyfill_script, + base_tag, + &html_str[insert_pos..] + ) } else { - 500 + // Kein gefunden - prepend + format!("{}{}{}", polyfill_script, base_tag, html_str) }; - Response::builder() - .status(status_code) - .body(Vec::new()) - .map_err(|e| e.into()) + content = modified_html.into_bytes(); + + // Cache die modifizierte HTML (aus deinem alten Code) + if let Ok(mut cache_guard) = HTML_CACHE.lock() { + cache_guard.insert(cache_key, content.clone()); + println!("Cached modified HTML for future requests"); + } } } - } else { - eprintln!( - "Asset nicht gefunden oder ist kein File: {}", - absolute_secure_path.display() + + let content_length = content.len(); + println!( + "Liefere {} ({}, {} bytes) ", + absolute_secure_path.display(), + mime_type, + content_length ); Response::builder() - .status(404) + .status(200) + .header("Content-Type", &mime_type) + .header("Content-Length", content_length.to_string()) + .header("Accept-Ranges", "bytes") + .header( + "X-HaexHub-Cache", + if asset_to_load == "index.html" && mime_type.contains("html") { + "MISS" + } else { + "N/A" + }, + ) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .header("Access-Control-Allow-Credentials", "true") + .body(content) + .map_err(|e| e.into()) + } + Err(e) => { + eprintln!( + "Fehler beim Lesen der Datei {}: {}", + absolute_secure_path.display(), + e + ); + let status_code = if e.kind() == std::io::ErrorKind::NotFound { + 404 + } else if e.kind() == std::io::ErrorKind::PermissionDenied { + 403 + } else { + 500 + }; + Response::builder() + .status(status_code) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") .body(Vec::new()) .map_err(|e| e.into()) } } - Err(e) => { - eprintln!("Fehler bei der Datenverarbeitung: {}", e); - - Response::builder() - .status(500) - .body(Vec::new()) - .map_err(|e| e.into()) - } + } else { + eprintln!( + "Asset nicht gefunden oder ist kein File: {}", + absolute_secure_path.display() + ); + Response::builder() + .status(404) + .header("Access-Control-Allow-Origin", allowed_origin) + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "*") + .body(Vec::new()) + .map_err(|e| e.into()) } } @@ -256,3 +945,131 @@ fn process_hex_encoded_json(hex_input: &str) -> Result Result { + // Return direkt ExtensionInfo (dekodiert) + // 1-3. Bestehende Fallbacks (wie vorher, aber return decoded Info statt hex) + if !origin.is_empty() && origin != "null" { + if let Ok(hex) = parse_from_origin(origin) { + if let Ok(info) = process_hex_encoded_json(&hex) { + cache_extension_info(&info); // Cache setzen + println!("Parsed und gecached aus Origin: {}", hex); + return Ok(info); + } + } + } + + println!("Fallback zu URI-Parsing"); + if let Ok(hex) = parse_from_uri_path(uri_ref) { + if let Ok(info) = process_hex_encoded_json(&hex) { + cache_extension_info(&info); // Cache setzen + println!("Parsed und gecached aus URI: {}", hex); + return Ok(info); + } + } + + println!("Fallback zu Referer-Parsing: {}", referer); + if !referer.is_empty() && referer != "null" { + if let Ok(hex) = parse_from_uri_string(referer) { + if let Ok(info) = process_hex_encoded_json(&hex) { + cache_extension_info(&info); // Cache setzen + println!("Parsed und gecached aus Referer: {}", hex); + return Ok(info); + } + } + } + + // 4. Fallback: Globaler Cache (für Assets in derselben Session) + println!("Fallback zu Cache"); + if let Some(cached_info) = get_cached_extension_info() { + println!( + "Gecached Info verwendet: KeyHash={}, Name={}, Version={}", + cached_info.key_hash, cached_info.name, cached_info.version + ); + return Ok(cached_info); + } + + Err( + "Kein gültiger Hex in Origin, URI, Referer oder Cache gefunden" + .to_string() + .into(), + ) +} + +// NEU: Cache-Helper (Mutex-sicher) +fn cache_extension_info(info: &ExtensionInfo) { + if let Ok(mut cache) = EXTENSION_CACHE.lock() { + *cache = Some(info.clone()); + } +} + +fn get_cached_extension_info() -> Option { + if let Ok(cache) = EXTENSION_CACHE.lock() { + cache.clone() + } else { + None + } +} + +fn parse_hex_from_url_string(url_str: &str) -> Result { + // Suche nach Scheme-Ende (://) + let scheme_end = match url_str.find("://") { + Some(pos) => pos + 3, // Nach "://" + _none => return Err("Kein Scheme in URL".to_string().into()), + }; + + let after_scheme = &url_str[scheme_end..]; + let path_start = match after_scheme.find('/') { + Some(pos) => pos, + _none => return Err("Kein Path in URL".to_string().into()), + }; + + let path = &after_scheme[path_start..]; // z.B. "/7b22.../index.html" + let mut segments = path.split('/').filter(|s| !s.is_empty()); + + let first_segment = match segments.next() { + Some(seg) => seg, + _none => return Err("Kein Path-Segment in URL".to_string().into()), + }; + + validate_and_return_hex(first_segment) +} + +// Vereinfachte parse_from_origin +fn parse_from_origin(origin: &str) -> Result { + parse_hex_from_url_string(origin) +} + +// Vereinfachte parse_from_uri_path +fn parse_from_uri_path(uri_ref: &Uri) -> Result { + let uri_str = uri_ref.to_string(); + parse_hex_from_url_string(&uri_str) +} + +// Vereinfachte parse_from_uri_string (für Referer) +fn parse_from_uri_string(uri_str: &str) -> Result { + parse_hex_from_url_string(uri_str) +} + +// validate_and_return_hex bleibt unverändert (aus letztem Vorschlag) +fn validate_and_return_hex(segment: &str) -> Result { + if segment.is_empty() { + return Err("Kein Extension-Info (hex) im Path".to_string().into()); + } + if segment.len() % 2 != 0 { + return Err("Ungültiger Hex: Ungerade Länge".to_string().into()); + } + if !segment.chars().all(|c| c.is_ascii_hexdigit()) { + return Err("Ungültiger Hex: Ungültige Zeichen".to_string().into()); + } + Ok(segment.to_string()) +} + +fn encode_hex_for_log(info: &ExtensionInfo) -> String { + let json_str = serde_json::to_string(info).unwrap_or_default(); + hex::encode(json_str.as_bytes()) +} diff --git a/src-tauri/src/extension/error.rs b/src-tauri/src/extension/error.rs index 67ca8f4..bed0746 100644 --- a/src-tauri/src/extension/error.rs +++ b/src-tauri/src/extension/error.rs @@ -12,6 +12,7 @@ pub enum ExtensionErrorCode { MutexPoisoned = 1003, Database = 2000, Filesystem = 2001, + FilesystemWithPath = 2004, Http = 2002, Shell = 2003, Manifest = 3000, @@ -60,6 +61,12 @@ pub enum ExtensionError { source: std::io::Error, }, + #[error("Filesystem operation failed at '{path}': {source}")] + FilesystemWithPath { + path: String, + source: std::io::Error, + }, + #[error("HTTP request failed: {reason}")] Http { reason: String }, @@ -109,6 +116,7 @@ impl ExtensionError { ExtensionError::PermissionDenied { .. } => ExtensionErrorCode::PermissionDenied, ExtensionError::Database { .. } => ExtensionErrorCode::Database, ExtensionError::Filesystem { .. } => ExtensionErrorCode::Filesystem, + ExtensionError::FilesystemWithPath { .. } => ExtensionErrorCode::FilesystemWithPath, ExtensionError::Http { .. } => ExtensionErrorCode::Http, ExtensionError::Shell { .. } => ExtensionErrorCode::Shell, ExtensionError::ManifestError { .. } => ExtensionErrorCode::Manifest, @@ -146,6 +154,14 @@ impl ExtensionError { _ => None, } } + + /// Helper to create a filesystem error with path context + pub fn filesystem_with_path>(path: P, source: std::io::Error) -> Self { + Self::FilesystemWithPath { + path: path.into(), + source, + } + } } impl serde::Serialize for ExtensionError { diff --git a/src-tauri/src/extension/mod.rs b/src-tauri/src/extension/mod.rs index 59ccc41..5849fc4 100644 --- a/src-tauri/src/extension/mod.rs +++ b/src-tauri/src/extension/mod.rs @@ -28,7 +28,29 @@ pub fn get_extension_info( } #[tauri::command] -pub fn get_all_extensions(state: State) -> Result, String> { +pub async fn get_all_extensions( + app_handle: AppHandle, + state: State<'_, AppState>, +) -> Result, String> { + // Check if extensions are loaded, if not load them first + let needs_loading = { + let prod_exts = state + .extension_manager + .production_extensions + .lock() + .unwrap(); + let dev_exts = state.extension_manager.dev_extensions.lock().unwrap(); + prod_exts.is_empty() && dev_exts.is_empty() + }; + + if needs_loading { + state + .extension_manager + .load_installed_extensions(&app_handle, &state) + .await + .map_err(|e| format!("Failed to load extensions: {:?}", e))?; + } + let mut extensions = Vec::new(); // Production Extensions @@ -57,18 +79,18 @@ pub fn get_all_extensions(state: State) -> Result, - extension_path: String, + file_bytes: Vec, ) -> Result { state .extension_manager - .preview_extension_internal(extension_path) + .preview_extension_internal(file_bytes) .await } #[tauri::command] pub async fn install_extension_with_permissions( app_handle: AppHandle, - source_path: String, + file_bytes: Vec, custom_permissions: EditablePermissions, state: State<'_, AppState>, ) -> Result { @@ -76,7 +98,7 @@ pub async fn install_extension_with_permissions( .extension_manager .install_extension_with_permissions_internal( app_handle, - source_path, + file_bytes, custom_permissions, &state, ) @@ -177,6 +199,18 @@ pub async fn remove_extension( .await } +#[tauri::command] +pub async fn remove_extension_by_full_id( + app_handle: AppHandle, + full_extension_id: String, + state: State<'_, AppState>, +) -> Result<(), ExtensionError> { + state + .extension_manager + .remove_extension_by_full_id(&app_handle, &full_extension_id, &state) + .await +} + #[tauri::command] pub fn is_extension_installed( extension_id: String, diff --git a/src-tauri/src/extension/permissions/types.rs b/src-tauri/src/extension/permissions/types.rs index d5aa549..ed78da9 100644 --- a/src-tauri/src/extension/permissions/types.rs +++ b/src-tauri/src/extension/permissions/types.rs @@ -7,7 +7,7 @@ use ts_rs::TS; /// Definiert Aktionen, die auf eine Datenbank angewendet werden können. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "camelCase")] #[ts(export)] pub enum DbAction { Read, @@ -38,9 +38,10 @@ impl FromStr for DbAction { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "read" => Ok(DbAction::Read), - "read_write" => Ok(DbAction::ReadWrite), + "readwrite" | "read_write" => Ok(DbAction::ReadWrite), "create" => Ok(DbAction::Create), "delete" => Ok(DbAction::Delete), + "alterdrop" | "alter_drop" => Ok(DbAction::AlterDrop), _ => Err(ExtensionError::InvalidActionString { input: s.to_string(), resource_type: "database".to_string(), @@ -51,7 +52,7 @@ impl FromStr for DbAction { /// Definiert Aktionen, die auf das Dateisystem angewendet werden können. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "camelCase")] #[ts(export)] pub enum FsAction { Read, @@ -76,7 +77,7 @@ impl FromStr for FsAction { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "read" => Ok(FsAction::Read), - "read_write" => Ok(FsAction::ReadWrite), + "readwrite" | "read_write" => Ok(FsAction::ReadWrite), _ => Err(ExtensionError::InvalidActionString { input: s.to_string(), resource_type: "filesystem".to_string(), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cd0cde1..021d5da 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -81,6 +81,7 @@ pub fn run() { extension::is_extension_installed, extension::preview_extension, extension::remove_extension, + extension::remove_extension_by_full_id, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e67f0c3..10584fc 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,6 +9,7 @@ "beforeBuildCommand": "pnpm generate", "frontendDist": "../dist" }, + "app": { "windows": [ { @@ -19,25 +20,40 @@ ], "security": { "csp": { - "default-src": ["'self'", "http://tauri.localhost"], + "default-src": ["'self'", "http://tauri.localhost", "haex-extension:"], "script-src": [ "'self'", "http://tauri.localhost", + "haex-extension:", "'wasm-unsafe-eval'" ], - "style-src": ["'self'", "http://tauri.localhost", "'unsafe-inline'"], + "style-src": [ + "'self'", + "http://tauri.localhost", + "haex-extension:", + "'unsafe-inline'" + ], "connect-src": [ "'self'", "http://tauri.localhost", + "haex-extension:", "ipc:", - "http://ipc.localhost" + "http://ipc.localhost", + "ws://localhost:*" ], - "img-src": ["'self'", "http://tauri.localhost", "data:", "blob:"], - "font-src": ["'self'", "http://tauri.localhost"], + "img-src": [ + "'self'", + "http://tauri.localhost", + "haex-extension:", + "data:", + "blob:" + ], + "font-src": ["'self'", "http://tauri.localhost", "haex-extension:"], "object-src": ["'none'"], - "media-src": ["'self'", "http://tauri.localhost"], - "frame-src": ["'none'"], - "frame-ancestors": ["'none'"] + "media-src": ["'self'", "http://tauri.localhost", "haex-extension:"], + "frame-src": ["haex-extension:"], + "frame-ancestors": ["'none'"], + "base-uri": ["'self'"] }, "assetProtocol": { "enable": true, diff --git a/src/app.vue b/src/app.vue index 04084e2..d997303 100644 --- a/src/app.vue +++ b/src/app.vue @@ -9,6 +9,9 @@