refactored install dialog

This commit is contained in:
2025-10-07 00:41:21 +02:00
parent 225835e5d1
commit c8c3a5c73f
44 changed files with 1426 additions and 730 deletions

View File

@ -1,5 +1,7 @@
//import tailwindcss from '@tailwindcss/vite' //import tailwindcss from '@tailwindcss/vite'
import { fileURLToPath } from 'node:url'
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: '2025-07-15', compatibilityDate: '2025-07-15',
@ -7,6 +9,12 @@ export default defineNuxtConfig({
srcDir: './src', srcDir: './src',
alias: {
'@bindings': fileURLToPath(
new URL('./src-tauri/bindings', import.meta.url),
),
},
app: { app: {
pageTransition: { pageTransition: {
name: 'fade', name: 'fade',

View File

@ -6,15 +6,16 @@
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
"dev": "nuxt dev", "dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"tauri": "tauri",
"tauri:build:debug": "tauri build --debug",
"generate:rust-types": "tsx ./src-tauri/database/generate-rust-types.ts",
"drizzle:generate": "drizzle-kit generate", "drizzle:generate": "drizzle-kit generate",
"drizzle:migrate": "drizzle-kit migrate", "drizzle:migrate": "drizzle-kit migrate",
"eslint:fix": "eslint --fix" "eslint:fix": "eslint --fix",
"generate:rust-types": "tsx ./src-tauri/database/generate-rust-types.ts",
"generate:ts-types": "cd src-tauri && cargo test",
"generate": "nuxt generate",
"postinstall": "nuxt prepare",
"preview": "nuxt preview",
"tauri:build:debug": "tauri build --debug",
"tauri": "tauri"
}, },
"dependencies": { "dependencies": {
"@nuxt/eslint": "1.9.0", "@nuxt/eslint": "1.9.0",

4
src-tauri/Cargo.lock generated
View File

@ -5264,9 +5264,9 @@ dependencies = [
[[package]] [[package]]
name = "uhlc" name = "uhlc"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66bbb93b0c2258fe1e81a84d8de5391f2577b039decabf75a6441ea1ebbf4cb5" checksum = "b62a645e3e4e6c85b7abe49b086aa3204119431f42b6123b0070419fb6e9d24e"
dependencies = [ dependencies = [
"humantime", "humantime",
"lazy_static", "lazy_static",

View File

@ -48,8 +48,8 @@ tauri-plugin-os = "2.3"
tauri-plugin-persisted-scope = "2.3.2" tauri-plugin-persisted-scope = "2.3.2"
tauri-plugin-store = "2.4.0" tauri-plugin-store = "2.4.0"
thiserror = "2.0.17" thiserror = "2.0.17"
ts-rs = "11.0.1" ts-rs = { version = "11.0.1", features = ["serde-compat"] }
uhlc = "0.8" uhlc = "0.8.2"
uuid = { version = "1.18.1", features = ["v4"] } uuid = { version = "1.18.1", features = ["v4"] }
zip = "5.1.1" zip = "5.1.1"
url = "2.5.7" url = "2.5.7"

View File

@ -0,0 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DbAction } from "./DbAction";
import type { FsAction } from "./FsAction";
import type { HttpAction } from "./HttpAction";
import type { ShellAction } from "./ShellAction";
/**
* Ein typsicherer Container, der die spezifische Aktion für einen Ressourcentyp enthält.
*/
export type Action = { "Database": DbAction } | { "Filesystem": FsAction } | { "Http": HttpAction } | { "Shell": ShellAction };

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DatabaseError = { "type": "ParseError", "details": { reason: string, sql: string, } } | { "type": "ParameterMismatchError", "details": { expected: number, provided: number, sql: string, } } | { "type": "NoTableError", "details": { sql: string, } } | { "type": "StatementError", "details": { reason: string, } } | { "type": "PrepareError", "details": { reason: string, } } | { "type": "DatabaseError", "details": { reason: string, } } | { "type": "ExecutionError", "details": { sql: string, reason: string, table: string | null, } } | { "type": "TransactionError", "details": { reason: string, } } | { "type": "UnsupportedStatement", "details": { reason: string, sql: string, } } | { "type": "HlcError", "details": { reason: string, } } | { "type": "LockError", "details": { reason: string, } } | { "type": "ConnectionError", "details": { reason: string, } } | { "type": "SerializationError", "details": { reason: string, } } | { "type": "PermissionError", "details": { extension_id: string, operation: string | null, resource: string | null, reason: string, } } | { "type": "QueryError", "details": { reason: string, } } | { "type": "RowProcessingError", "details": { reason: string, } } | { "type": "MutexPoisoned", "details": { reason: string, } } | { "type": "ConnectionFailed", "details": { path: string, reason: string, } } | { "type": "PragmaError", "details": { pragma: string, reason: string, } } | { "type": "PathResolutionError", "details": { reason: string, } } | { "type": "IoError", "details": { path: string, reason: string, } } | { "type": "CrdtSetup", "details": string };

View File

@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Definiert Aktionen, die auf eine Datenbank angewendet werden können.
*/
export type DbAction = "read" | "readwrite" | "create" | "delete" | "alterdrop";

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DbConstraints = { where_clause: string | null, columns: Array<string> | null, limit: number | null, };

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ExtensionInfoResponse = { key_hash: string, name: string, full_id: string, version: string, display_name: string | null, namespace: string | null, allowed_origin: string, };

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ExtensionPermissions } from "./ExtensionPermissions";
export type ExtensionManifest = { id: string, 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, };

View File

@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PermissionEntry } from "./PermissionEntry";
/**
* Definiert die einheitliche Struktur für alle Berechtigungsarten im Manifest und UI.
*/
export type ExtensionPermissions = { database: Array<PermissionEntry> | null, filesystem: Array<PermissionEntry> | null, http: Array<PermissionEntry> | null, shell: Array<PermissionEntry> | null, };

View File

@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ExtensionManifest } from "./ExtensionManifest";
import type { ExtensionPermissions } from "./ExtensionPermissions";
export type ExtensionPreview = { manifest: ExtensionManifest, is_valid_signature: boolean, key_hash: string, editable_permissions: ExtensionPermissions, };

View File

@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Definiert Aktionen, die auf das Dateisystem angewendet werden können.
*/
export type FsAction = "read" | "readwrite";

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FsConstraints = { max_file_size: bigint | null, allowed_extensions: Array<string> | null, recursive: boolean | null, };

View File

@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Definiert Aktionen (HTTP-Methoden), die auf HTTP-Anfragen angewendet werden können.
*/
export type HttpAction = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "*";

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { RateLimit } from "./RateLimit";
export type HttpConstraints = { methods: Array<string> | null, rate_limit: RateLimit | null, };

View File

@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DbConstraints } from "./DbConstraints";
import type { FsConstraints } from "./FsConstraints";
import type { HttpConstraints } from "./HttpConstraints";
import type { ShellConstraints } from "./ShellConstraints";
export type PermissionConstraints = DbConstraints | FsConstraints | HttpConstraints | ShellConstraints;

View File

@ -0,0 +1,19 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PermissionStatus } from "./PermissionStatus";
/**
* Repräsentiert einen einzelnen Berechtigungseintrag im Manifest und im UI-Modell.
*/
export type PermissionEntry = { target: string,
/**
* Die auszuführende Aktion (z.B. "read", "read_write", "GET", "execute").
*/
operation?: string | null,
/**
* Optionale, spezifische Einschränkungen für diese Berechtigung.
*/
constraints?: Record<string, unknown>,
/**
* Der Status der Berechtigung (wird nur im UI-Modell verwendet).
*/
status?: PermissionStatus | null, };

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PermissionStatus = "ask" | "granted" | "denied";

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RateLimit = { requests: number, per_minutes: number, };

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ResourceType = "fs" | "http" | "db" | "shell";

View File

@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Definiert Aktionen, die auf Shell-Befehle angewendet werden können.
*/
export type ShellAction = "execute";

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ShellConstraints = { allowed_subcommands: Array<string> | null, allowed_flags: Array<string> | null, forbidden_args: Array<string> | null, };

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type VaultInfo = { name: string, lastAccess: bigint, path: string, };

View File

@ -9,7 +9,7 @@ use rusqlite::{
Connection, OpenFlags, ToSql, Connection, OpenFlags, ToSql,
}; };
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use sqlparser::ast::{Query, Select, SetExpr, Statement, TableFactor, TableObject}; use sqlparser::ast::{Expr, Query, Select, SetExpr, Statement, TableFactor, TableObject};
use sqlparser::dialect::SQLiteDialect; use sqlparser::dialect::SQLiteDialect;
use sqlparser::parser::Parser; use sqlparser::parser::Parser;
use std::collections::HashMap; use std::collections::HashMap;
@ -328,8 +328,42 @@ fn extract_tables_from_select(select: &Select, tables: &mut Vec<String>) {
extract_tables_from_table_factor(&join.relation, tables); extract_tables_from_table_factor(&join.relation, tables);
} }
} }
if let Some(selection) = &select.selection {
extract_tables_from_expr_recursive(selection, tables);
}
} }
fn extract_tables_from_expr_recursive(expr: &Expr, tables: &mut Vec<String>) {
match expr {
// This is the key: we found a subquery!
Expr::Subquery(subquery) => {
extract_tables_from_query_recursive(subquery, tables);
}
// These expressions can contain other expressions
Expr::BinaryOp { left, right, .. } => {
extract_tables_from_expr_recursive(left, tables);
extract_tables_from_expr_recursive(right, tables);
}
Expr::UnaryOp { expr, .. } => {
extract_tables_from_expr_recursive(expr, tables);
}
Expr::InSubquery { expr, subquery, .. } => {
extract_tables_from_expr_recursive(expr, tables);
extract_tables_from_query_recursive(subquery, tables);
}
Expr::Between {
expr, low, high, ..
} => {
extract_tables_from_expr_recursive(expr, tables);
extract_tables_from_expr_recursive(low, tables);
extract_tables_from_expr_recursive(high, tables);
}
// ... other expression types can be added here if needed
_ => {
// Other expressions (like literals, column names, etc.) don't contain tables.
}
}
}
/// Extrahiert Tabellennamen aus TableFactor-Strukturen /// Extrahiert Tabellennamen aus TableFactor-Strukturen
fn extract_tables_from_table_factor(table_factor: &TableFactor, tables: &mut Vec<String>) { fn extract_tables_from_table_factor(table_factor: &TableFactor, tables: &mut Vec<String>) {
match table_factor { match table_factor {

View File

@ -1,14 +1,17 @@
// src-tauri/src/extension/core/manager.rs use crate::database::core::with_connection;
use crate::database::error::DatabaseError;
use crate::extension::core::manifest::{EditablePermissions, ExtensionManifest, ExtensionPreview}; use crate::extension::core::manifest::{EditablePermissions, ExtensionManifest, ExtensionPreview};
use crate::extension::core::types::{copy_directory, Extension, ExtensionSource}; use crate::extension::core::types::{copy_directory, Extension, ExtensionSource};
use crate::extension::core::ExtensionPermissions;
use crate::extension::crypto::ExtensionCrypto; use crate::extension::crypto::ExtensionCrypto;
use crate::extension::database::executor::SqlExecutor;
use crate::extension::error::ExtensionError; use crate::extension::error::ExtensionError;
use crate::extension::permissions::manager::PermissionManager; use crate::extension::permissions::manager::PermissionManager;
use crate::extension::permissions::types::{ExtensionPermission, PermissionStatus}; use crate::extension::permissions::types::ExtensionPermission;
use crate::table_names::TABLE_EXTENSIONS;
use crate::AppState; use crate::AppState;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::File; use std::fs::{self, File};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Mutex; use std::sync::Mutex;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
@ -22,11 +25,36 @@ pub struct CachedPermission {
pub ttl: Duration, pub ttl: Duration,
} }
#[derive(Debug, Clone)]
pub struct MissingExtension {
pub full_extension_id: String,
pub name: String,
pub version: String,
}
struct ExtensionDataFromDb {
manifest: ExtensionManifest,
enabled: bool,
}
#[derive(Default)] #[derive(Default)]
pub struct ExtensionManager { pub struct ExtensionManager {
pub production_extensions: Mutex<HashMap<String, Extension>>, pub production_extensions: Mutex<HashMap<String, Extension>>,
pub dev_extensions: Mutex<HashMap<String, Extension>>, pub dev_extensions: Mutex<HashMap<String, Extension>>,
pub permission_cache: Mutex<HashMap<String, CachedPermission>>, pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
pub missing_extensions: Mutex<Vec<MissingExtension>>,
}
struct ExtractedExtension {
temp_dir: PathBuf,
manifest: ExtensionManifest,
content_hash: String,
}
impl Drop for ExtractedExtension {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.temp_dir).ok();
}
} }
impl ExtensionManager { impl ExtensionManager {
@ -34,6 +62,49 @@ impl ExtensionManager {
Self::default() Self::default()
} }
/// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest
fn extract_and_validate_extension(
source_path: &str,
temp_prefix: &str,
) -> Result<ExtractedExtension, ExtensionError> {
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 })?;
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
let mut archive =
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Invalid ZIP: {}", e),
})?;
archive
.extract(&temp)
.map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Cannot extract ZIP: {}", e),
})?;
let manifest_path = temp.join("manifest.json");
let manifest_content =
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read manifest: {}", e),
})?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
let content_hash = ExtensionCrypto::hash_directory(&temp).map_err(|e| {
ExtensionError::SignatureVerificationFailed {
reason: e.to_string(),
}
})?;
Ok(ExtractedExtension {
temp_dir: temp,
manifest,
content_hash,
})
}
pub fn get_base_extension_dir( pub fn get_base_extension_dir(
&self, &self,
app_handle: &AppHandle, app_handle: &AppHandle,
@ -45,23 +116,42 @@ impl ExtensionManager {
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()), source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
})? })?
.join("extensions"); .join("extensions");
// Sicherstellen, dass das Basisverzeichnis existiert
if !path.exists() {
fs::create_dir_all(&path).map_err(|e| ExtensionError::Filesystem { source: e })?;
}
Ok(path) Ok(path)
} }
pub fn get_extension_dir( pub fn get_extension_dir(
&self, &self,
app_handle: &AppHandle, app_handle: &AppHandle,
extension_id: &str, key_hash: &str,
extension_name: &str,
extension_version: &str, extension_version: &str,
) -> Result<PathBuf, ExtensionError> { ) -> Result<PathBuf, ExtensionError> {
let specific_extension_dir = self let specific_extension_dir = self
.get_base_extension_dir(app_handle)? .get_base_extension_dir(app_handle)?
.join(extension_id) .join(key_hash)
.join(extension_name)
.join(extension_version); .join(extension_version);
Ok(specific_extension_dir) Ok(specific_extension_dir)
} }
pub fn get_extension_path_by_full_extension_id(
&self,
app_handle: &AppHandle,
full_extension_id: &str,
) -> Result<PathBuf, ExtensionError> {
let specific_extension_dir = self
.get_base_extension_dir(app_handle)?
.join(full_extension_id);
Ok(specific_extension_dir)
}
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> { pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
if extension.id.is_empty() { if extension.id.is_empty() {
return Err(ExtensionError::ValidationError { return Err(ExtensionError::ValidationError {
@ -133,15 +223,40 @@ impl ExtensionManager {
pub async fn remove_extension_internal( pub async fn remove_extension_internal(
&self, &self,
app_handle: &AppHandle, app_handle: &AppHandle,
extension_id: String, key_hash: &str,
extension_version: String, extension_id: &str,
extension_version: &str,
state: &State<'_, AppState>, state: &State<'_, AppState>,
) -> Result<(), ExtensionError> { ) -> Result<(), ExtensionError> {
PermissionManager::delete_permissions(state, &extension_id).await?; // Lösche Permissions und Extension-Eintrag in einer Transaktion
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(),
})?;
// Lösche alle Permissions
PermissionManager::delete_permissions_in_transaction(&tx, &hlc_service, extension_id)?;
// Lösche Extension-Eintrag
let sql = format!("DELETE FROM {} WHERE id = ?", TABLE_EXTENSIONS);
SqlExecutor::execute_internal_typed(
&tx,
&hlc_service,
&sql,
rusqlite::params![extension_id],
)?;
tx.commit().map_err(DatabaseError::from)
})?;
// Entferne aus dem In-Memory-Manager
self.remove_extension(&extension_id)?; self.remove_extension(&extension_id)?;
// Lösche Dateien vom Dateisystem
let extension_dir = let extension_dir =
self.get_extension_dir(app_handle, &extension_id, &extension_version)?; self.get_extension_dir(app_handle, key_hash, extension_id, extension_version)?;
if extension_dir.exists() { if extension_dir.exists() {
std::fs::remove_dir_all(&extension_dir) std::fs::remove_dir_all(&extension_dir)
@ -155,48 +270,20 @@ impl ExtensionManager {
&self, &self,
source_path: String, source_path: String,
) -> Result<ExtensionPreview, ExtensionError> { ) -> Result<ExtensionPreview, ExtensionError> {
let source = PathBuf::from(&source_path); let extracted = Self::extract_and_validate_extension(&source_path, "haexhub_preview")?;
let temp = std::env::temp_dir().join(format!("haexhub_preview_{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
let mut archive =
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Invalid ZIP: {}", e),
})?;
archive
.extract(&temp)
.map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Cannot extract ZIP: {}", e),
})?;
let manifest_path = temp.join("manifest.json");
let manifest_content =
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read manifest: {}", e),
})?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
let content_hash = ExtensionCrypto::hash_directory(&temp)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
let is_valid_signature = ExtensionCrypto::verify_signature( let is_valid_signature = ExtensionCrypto::verify_signature(
&manifest.public_key, &extracted.manifest.public_key,
&content_hash, &extracted.content_hash,
&manifest.signature, &extracted.manifest.signature,
) )
.is_ok(); .is_ok();
let key_hash = manifest.calculate_key_hash()?; let key_hash = extracted.manifest.calculate_key_hash()?;
let editable_permissions = manifest.to_editable_permissions(); let editable_permissions = extracted.manifest.to_editable_permissions();
std::fs::remove_dir_all(&temp).ok();
Ok(ExtensionPreview { Ok(ExtensionPreview {
manifest, manifest: extracted.manifest.clone(),
is_valid_signature, is_valid_signature,
key_hash, key_hash,
editable_permissions, editable_permissions,
@ -210,78 +297,45 @@ impl ExtensionManager {
custom_permissions: EditablePermissions, custom_permissions: EditablePermissions,
state: &State<'_, AppState>, state: &State<'_, AppState>,
) -> Result<String, ExtensionError> { ) -> Result<String, ExtensionError> {
let source = PathBuf::from(&source_path); let extracted = Self::extract_and_validate_extension(&source_path, "haexhub_ext")?;
let temp = std::env::temp_dir().join(format!("haexhub_ext_{}", uuid::Uuid::new_v4())); // Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft)
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?; ExtensionCrypto::verify_signature(
&extracted.manifest.public_key,
&extracted.content_hash,
&extracted.manifest.signature,
)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?; let full_extension_id = extracted.manifest.full_extension_id()?;
let mut archive =
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Invalid ZIP: {}", e),
})?;
archive let extensions_dir = self.get_extension_dir(
.extract(&temp) &app_handle,
.map_err(|e| ExtensionError::InstallationFailed { &extracted.manifest.calculate_key_hash()?,
reason: format!("Cannot extract ZIP: {}", e), &extracted.manifest.name,
})?; &extracted.manifest.version,
)?;
let manifest_path = temp.join("manifest.json");
let manifest_content =
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read manifest: {}", e),
})?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
let content_hash = ExtensionCrypto::hash_directory(&temp)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
ExtensionCrypto::verify_signature(&manifest.public_key, &content_hash, &manifest.signature)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
let key_hash = manifest.calculate_key_hash()?;
let full_extension_id = format!("{}-{}", key_hash, manifest.id);
let extensions_dir = app_handle
.path()
.app_data_dir()
.map_err(|e| ExtensionError::Filesystem {
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
})?
.join("extensions")
.join(&full_extension_id)
.join(&manifest.version);
std::fs::create_dir_all(&extensions_dir) std::fs::create_dir_all(&extensions_dir)
.map_err(|e| ExtensionError::Filesystem { source: e })?; .map_err(|e| ExtensionError::Filesystem { source: e })?;
copy_directory( copy_directory(
temp.to_string_lossy().to_string(), extracted.temp_dir.to_string_lossy().to_string(),
extensions_dir.to_string_lossy().to_string(), extensions_dir.to_string_lossy().to_string(),
)?; )?;
std::fs::remove_dir_all(&temp).ok();
let permissions = custom_permissions.to_internal_permissions(&full_extension_id); let permissions = custom_permissions.to_internal_permissions(&full_extension_id);
let granted_permissions: Vec<_> = permissions PermissionManager::save_permissions(state, &permissions).await?;
.into_iter()
.filter(|p| p.status == PermissionStatus::Granted)
.collect();
PermissionManager::save_permissions(state, &full_extension_id, &granted_permissions)
.await?;
let extension = Extension { let extension = Extension {
id: full_extension_id.clone(), id: full_extension_id.clone(),
name: manifest.name.clone(), name: extracted.manifest.name.clone(),
source: ExtensionSource::Production { source: ExtensionSource::Production {
path: extensions_dir.clone(), path: extensions_dir.clone(),
version: manifest.version.clone(), version: extracted.manifest.version.clone(),
}, },
manifest: manifest.clone(), manifest: extracted.manifest.clone(),
enabled: true, enabled: true,
last_accessed: SystemTime::now(), last_accessed: SystemTime::now(),
}; };
@ -290,22 +344,116 @@ impl ExtensionManager {
Ok(full_extension_id) Ok(full_extension_id)
} }
}
// Backward compatibility /// Scannt das Dateisystem beim Start und lädt alle installierten Erweiterungen.
#[derive(Default)] pub async fn load_installed_extensions(
pub struct ExtensionState { &self,
pub extensions: Mutex<HashMap<String, ExtensionManifest>>, app_handle: &AppHandle,
} state: &State<'_, AppState>,
) -> Result<Vec<String>, ExtensionError> {
self.production_extensions
.lock()
.map_err(|e| ExtensionError::MutexPoisoned {
reason: e.to_string(),
})?
.clear();
self.permission_cache
.lock()
.map_err(|e| ExtensionError::MutexPoisoned {
reason: e.to_string(),
})?
.clear();
self.missing_extensions
.lock()
.map_err(|e| ExtensionError::MutexPoisoned {
reason: e.to_string(),
})?
.clear();
impl ExtensionState { // Schritt 1: Alle Daten aus der Datenbank in einem Rutsch laden.
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) { let extensions = with_connection(&state.db, |conn| {
let mut extensions = self.extensions.lock().unwrap(); let sql = "SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled FROM haexExtensions";
extensions.insert(path, manifest); let results = SqlExecutor::select_internal(conn, sql, &[])?;
}
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> { let mut data = Vec::new();
let extensions = self.extensions.lock().unwrap(); for result in results {
extensions.values().find(|p| p.name == addon_id).cloned() let manifest = ExtensionManifest {
id: result["id"]
.as_str()
.ok_or_else(|| DatabaseError::SerializationError {
reason: "Missing id field".to_string(),
})?
.to_string(),
name: result["name"]
.as_str()
.ok_or_else(|| DatabaseError::SerializationError {
reason: "Missing name field".to_string(),
})?
.to_string(),
version: result["version"]
.as_str()
.ok_or_else(|| DatabaseError::SerializationError {
reason: "Missing version field".to_string(),
})?
.to_string(),
author: result["author"].as_str().map(String::from),
entry: result["entry"].as_str().unwrap_or("index.html").to_string(),
icon: result["icon"].as_str().map(String::from),
public_key: result["public_key"].as_str().unwrap_or("").to_string(),
signature: result["signature"].as_str().unwrap_or("").to_string(),
permissions: ExtensionPermissions::default(),
homepage: result["homepage"].as_str().map(String::from),
description: result["description"].as_str().map(String::from),
};
let enabled = result["enabled"]
.as_bool()
.or_else(|| result["enabled"].as_i64().map(|v| v != 0))
.unwrap_or(false);
data.push(ExtensionDataFromDb { manifest, enabled });
}
Ok(data)
})?;
// 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()?;
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() {
self.missing_extensions
.lock()
.map_err(|e| ExtensionError::MutexPoisoned {
reason: e.to_string(),
})?
.push(MissingExtension {
full_extension_id: full_extension_id.clone(),
name: extension.manifest.name.clone(),
version: extension.manifest.version.clone(),
});
continue;
}
let extension = Extension {
id: full_extension_id.clone(),
name: extension.manifest.name.clone(),
source: ExtensionSource::Production {
path: extension_path,
version: extension.manifest.version.clone(),
},
manifest: extension.manifest,
enabled: extension.enabled,
last_accessed: SystemTime::now(),
};
self.add_production_extension(extension)?;
loaded_extension_ids.push(full_extension_id);
}
Ok(loaded_extension_ids)
} }
} }

View File

@ -1,14 +1,60 @@
// src-tauri/src/extension/core/manifest.rs
use crate::extension::crypto::ExtensionCrypto; use crate::extension::crypto::ExtensionCrypto;
use crate::extension::error::ExtensionError; use crate::extension::error::ExtensionError;
use crate::extension::permissions::types::{ use crate::extension::permissions::types::{
Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints, Action, DbAction, ExtensionPermission, FsAction, HttpAction, PermissionConstraints,
PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints, PermissionStatus, ResourceType, ShellAction,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr;
use ts_rs::TS;
#[derive(Serialize, Deserialize, Clone, Debug)] /// Repräsentiert einen einzelnen Berechtigungseintrag im Manifest und im UI-Modell.
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
#[ts(export)]
pub struct PermissionEntry {
pub target: String,
/// Die auszuführende Aktion (z.B. "read", "read_write", "GET", "execute").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operation: Option<String>,
/// Optionale, spezifische Einschränkungen für diese Berechtigung.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "Record<string, unknown>")]
pub constraints: Option<serde_json::Value>,
/// Der Status der Berechtigung (wird nur im UI-Modell verwendet).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<PermissionStatus>,
}
#[derive(Serialize, Deserialize, TS)]
#[ts(export)]
pub struct ExtensionPreview {
pub manifest: ExtensionManifest,
pub is_valid_signature: bool,
pub key_hash: String,
pub editable_permissions: EditablePermissions,
}
/// Definiert die einheitliche Struktur für alle Berechtigungsarten im Manifest und UI.
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
#[ts(export)]
pub struct ExtensionPermissions {
#[serde(default)]
pub database: Option<Vec<PermissionEntry>>,
#[serde(default)]
pub filesystem: Option<Vec<PermissionEntry>>,
#[serde(default)]
pub http: Option<Vec<PermissionEntry>>,
#[serde(default)]
pub shell: Option<Vec<PermissionEntry>>,
}
/// Typ-Alias für bessere Lesbarkeit, wenn die Struktur als UI-Modell verwendet wird.
pub type EditablePermissions = ExtensionPermissions;
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
#[ts(export)]
pub struct ExtensionManifest { pub struct ExtensionManifest {
pub id: String, pub id: String,
pub name: String, pub name: String,
@ -18,7 +64,7 @@ pub struct ExtensionManifest {
pub icon: Option<String>, pub icon: Option<String>,
pub public_key: String, pub public_key: String,
pub signature: String, pub signature: String,
pub permissions: ExtensionManifestPermissions, pub permissions: ExtensionPermissions,
pub homepage: Option<String>, pub homepage: Option<String>,
pub description: Option<String>, pub description: Option<String>,
} }
@ -31,192 +77,104 @@ impl ExtensionManifest {
pub fn full_extension_id(&self) -> Result<String, ExtensionError> { pub fn full_extension_id(&self) -> Result<String, ExtensionError> {
let key_hash = self.calculate_key_hash()?; let key_hash = self.calculate_key_hash()?;
Ok(format!("{}-{}", key_hash, self.id)) Ok(format!("{}_{}_{}", key_hash, self.name, self.version))
} }
/// Konvertiert die Manifest-Berechtigungen in das bearbeitbare UI-Modell,
/// indem der Standardstatus `Granted` gesetzt wird.
pub fn to_editable_permissions(&self) -> EditablePermissions { pub fn to_editable_permissions(&self) -> EditablePermissions {
let mut editable = self.permissions.clone();
let set_status_for_list = |list: Option<&mut Vec<PermissionEntry>>| {
if let Some(entries) = list {
for entry in entries.iter_mut() {
entry.status = Some(PermissionStatus::Granted);
}
}
};
set_status_for_list(editable.database.as_mut());
set_status_for_list(editable.filesystem.as_mut());
set_status_for_list(editable.http.as_mut());
set_status_for_list(editable.shell.as_mut());
editable
}
}
impl ExtensionPermissions {
/// Konvertiert das UI-Modell in die flache Liste von internen `ExtensionPermission`-Objekten.
pub fn to_internal_permissions(&self, extension_id: &str) -> Vec<ExtensionPermission> {
let mut permissions = Vec::new(); let mut permissions = Vec::new();
if let Some(db) = &self.permissions.database { if let Some(entries) = &self.database {
for resource in &db.read { for p in entries {
permissions.push(EditablePermission { if let Some(perm) = Self::create_internal(extension_id, ResourceType::Db, p) {
resource_type: "db".to_string(), permissions.push(perm);
action: "read".to_string(), }
target: resource.clone(),
constraints: None,
status: "granted".to_string(),
});
} }
for resource in &db.write { }
permissions.push(EditablePermission { if let Some(entries) = &self.filesystem {
resource_type: "db".to_string(), for p in entries {
action: "write".to_string(), if let Some(perm) = Self::create_internal(extension_id, ResourceType::Fs, p) {
target: resource.clone(), permissions.push(perm);
constraints: None, }
status: "granted".to_string(), }
}); }
if let Some(entries) = &self.http {
for p in entries {
if let Some(perm) = Self::create_internal(extension_id, ResourceType::Http, p) {
permissions.push(perm);
}
}
}
if let Some(entries) = &self.shell {
for p in entries {
if let Some(perm) = Self::create_internal(extension_id, ResourceType::Shell, p) {
permissions.push(perm);
}
} }
} }
if let Some(fs) = &self.permissions.filesystem { permissions
for path in &fs.read { }
permissions.push(EditablePermission {
resource_type: "fs".to_string(),
action: "read".to_string(),
target: path.clone(),
constraints: None,
status: "granted".to_string(),
});
}
for path in &fs.write {
permissions.push(EditablePermission {
resource_type: "fs".to_string(),
action: "write".to_string(),
target: path.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
if let Some(http_list) = &self.permissions.http { /// Parst einen einzelnen `PermissionEntry` und wandelt ihn in die interne, typsichere `ExtensionPermission`-Struktur um.
for domain in http_list { fn create_internal(
permissions.push(EditablePermission { extension_id: &str,
resource_type: "http".to_string(), resource_type: ResourceType,
action: "read".to_string(), p: &PermissionEntry,
target: domain.clone(), ) -> Option<ExtensionPermission> {
constraints: None, let operation_str = p.operation.as_deref().unwrap_or_default();
status: "granted".to_string(),
});
}
}
if let Some(shell_list) = &self.permissions.shell { let action = match resource_type {
for command in shell_list { ResourceType::Db => DbAction::from_str(operation_str).ok().map(Action::Database),
permissions.push(EditablePermission { ResourceType::Fs => FsAction::from_str(operation_str)
resource_type: "shell".to_string(), .ok()
action: "read".to_string(), .map(Action::Filesystem),
target: command.clone(), ResourceType::Http => HttpAction::from_str(operation_str).ok().map(Action::Http),
constraints: None, ResourceType::Shell => ShellAction::from_str(operation_str).ok().map(Action::Shell),
status: "granted".to_string(), };
});
}
}
EditablePermissions { permissions } action.map(|act| ExtensionPermission {
id: uuid::Uuid::new_v4().to_string(),
extension_id: extension_id.to_string(),
resource_type: resource_type.clone(),
action: act,
target: p.target.clone(),
constraints: p
.constraints
.as_ref()
.and_then(|c| serde_json::from_value::<PermissionConstraints>(c.clone()).ok()),
status: p.status.clone().unwrap_or(PermissionStatus::Ask),
haex_timestamp: None,
haex_tombstone: None,
})
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, Default)] #[derive(Serialize, Deserialize, Clone, Debug, TS)]
pub struct ExtensionManifestPermissions { #[ts(export)]
#[serde(default)]
pub database: Option<DatabaseManifestPermissions>,
#[serde(default)]
pub filesystem: Option<FilesystemManifestPermissions>,
#[serde(default)]
pub http: Option<Vec<String>>,
#[serde(default)]
pub shell: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct DatabaseManifestPermissions {
#[serde(default)]
pub read: Vec<String>,
#[serde(default)]
pub write: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct FilesystemManifestPermissions {
#[serde(default)]
pub read: Vec<String>,
#[serde(default)]
pub write: Vec<String>,
}
// Editable Permissions für UI
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EditablePermissions {
pub permissions: Vec<EditablePermission>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EditablePermission {
pub resource_type: String,
pub action: String,
pub target: String,
pub constraints: Option<serde_json::Value>,
pub status: String,
}
impl EditablePermissions {
pub fn to_internal_permissions(&self, extension_id: &str) -> Vec<ExtensionPermission> {
self.permissions
.iter()
.map(|p| ExtensionPermission {
id: uuid::Uuid::new_v4().to_string(),
extension_id: extension_id.to_string(),
resource_type: match p.resource_type.as_str() {
"fs" => ResourceType::Fs,
"http" => ResourceType::Http,
"db" => ResourceType::Db,
"shell" => ResourceType::Shell,
_ => ResourceType::Fs,
},
action: match p.action.as_str() {
"read" => Action::Read,
"write" => Action::Write,
_ => Action::Read,
},
target: p.target.clone(),
constraints: p
.constraints
.as_ref()
.and_then(|c| Self::parse_constraints(&p.resource_type, c)),
status: match p.status.as_str() {
"granted" => PermissionStatus::Granted,
"denied" => PermissionStatus::Denied,
"ask" => PermissionStatus::Ask,
_ => PermissionStatus::Denied,
},
haex_timestamp: None,
haex_tombstone: None,
})
.collect()
}
fn parse_constraints(
resource_type: &str,
json_value: &serde_json::Value,
) -> Option<PermissionConstraints> {
match resource_type {
"db" => serde_json::from_value::<DbConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Database),
"fs" => serde_json::from_value::<FsConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Filesystem),
"http" => serde_json::from_value::<HttpConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Http),
"shell" => serde_json::from_value::<ShellConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Shell),
_ => None,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct ExtensionPreview {
pub manifest: ExtensionManifest,
pub is_valid_signature: bool,
pub key_hash: String,
pub editable_permissions: EditablePermissions,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionInfoResponse { pub struct ExtensionInfoResponse {
pub key_hash: String, pub key_hash: String,
pub name: String, pub name: String,
@ -231,6 +189,7 @@ impl ExtensionInfoResponse {
pub fn from_extension( pub fn from_extension(
extension: &crate::extension::core::types::Extension, extension: &crate::extension::core::types::Extension,
) -> Result<Self, ExtensionError> { ) -> Result<Self, ExtensionError> {
// Annahme: get_tauri_origin ist in deinem `types`-Modul oder woanders definiert
use crate::extension::core::types::get_tauri_origin; use crate::extension::core::types::get_tauri_origin;
let allowed_origin = get_tauri_origin(); let allowed_origin = get_tauri_origin();

View File

@ -12,7 +12,8 @@ use tauri::{AppHandle, State};
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct ExtensionInfo { struct ExtensionInfo {
id: String, key_hash: String,
name: String,
version: String, version: String,
} }
@ -66,17 +67,18 @@ impl From<serde_json::Error> for DataProcessingError {
pub fn resolve_secure_extension_asset_path( pub fn resolve_secure_extension_asset_path(
app_handle: &AppHandle, app_handle: &AppHandle,
state: State<AppState>, state: State<AppState>,
extension_id: &str, key_hash: &str,
extension_name: &str,
extension_version: &str, extension_version: &str,
requested_asset_path: &str, requested_asset_path: &str,
) -> Result<PathBuf, ExtensionError> { ) -> Result<PathBuf, ExtensionError> {
if extension_id.is_empty() if extension_name.is_empty()
|| !extension_id || !extension_name
.chars() .chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-') .all(|c| c.is_ascii_alphanumeric() || c == '-')
{ {
return Err(ExtensionError::ValidationError { return Err(ExtensionError::ValidationError {
reason: format!("Invalid extension ID: {}", extension_id), reason: format!("Invalid extension name: {}", extension_name),
}); });
} }
@ -90,10 +92,12 @@ pub fn resolve_secure_extension_asset_path(
}); });
} }
let specific_extension_dir = let specific_extension_dir = state.extension_manager.get_extension_dir(
state app_handle,
.extension_manager key_hash,
.get_extension_dir(app_handle, extension_id, extension_version)?; extension_name,
extension_version,
)?;
let clean_relative_path = requested_asset_path let clean_relative_path = requested_asset_path
.replace('\\', "/") .replace('\\', "/")
@ -169,12 +173,14 @@ pub fn extension_protocol_handler(
match process_hex_encoded_json(&host) { match process_hex_encoded_json(&host) {
Ok(info) => { Ok(info) => {
println!("Daten erfolgreich verarbeitet:"); println!("Daten erfolgreich verarbeitet:");
println!(" ID: {}", info.id); println!(" KeyHash: {}", info.key_hash);
println!(" Name: {}", info.name);
println!(" Version: {}", info.version); println!(" Version: {}", info.version);
let absolute_secure_path = resolve_secure_extension_asset_path( let absolute_secure_path = resolve_secure_extension_asset_path(
app_handle, app_handle,
state, state,
&info.id, &info.key_hash,
&info.name,
&info.version, &info.version,
&asset_to_load, &asset_to_load,
)?; )?;

View File

@ -1,3 +1,8 @@
use std::{
fs,
path::{Path, PathBuf},
};
// src-tauri/src/extension/crypto.rs // src-tauri/src/extension/crypto.rs
use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@ -45,30 +50,62 @@ impl ExtensionCrypto {
} }
/// Berechnet Hash eines Verzeichnisses (für Verifikation) /// Berechnet Hash eines Verzeichnisses (für Verifikation)
pub fn hash_directory(dir: &std::path::Path) -> Result<String, String> { pub fn hash_directory(dir: &Path) -> Result<String, String> {
use std::fs; // 1. Alle Dateipfade rekursiv sammeln
let mut all_files = Vec::new();
Self::collect_files_recursively(dir, &mut all_files)
.map_err(|e| format!("Failed to collect files: {}", e))?;
all_files.sort();
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
let mut entries: Vec<_> = fs::read_dir(dir) let manifest_path = dir.join("manifest.json");
.map_err(|e| format!("Cannot read directory: {}", e))?
.filter_map(|e| e.ok())
.collect();
// Sortieren für deterministische Hashes // 2. Inhalte der sortierten Dateien hashen
entries.sort_by_key(|e| e.path()); for file_path in all_files {
if file_path == manifest_path {
// FÜR DIE MANIFEST.JSON:
let content_str = fs::read_to_string(&file_path)
.map_err(|e| format!("Cannot read manifest file: {}", e))?;
for entry in entries { // Parse zu einem generischen JSON-Wert
let path = entry.path(); let mut manifest: serde_json::Value = serde_json::from_str(&content_str)
if path.is_file() { .map_err(|e| format!("Cannot parse manifest JSON: {}", e))?;
let content = fs::read(&path)
.map_err(|e| format!("Cannot read file {}: {}", path.display(), e))?; // Entferne oder leere das Signaturfeld, um den "kanonischen Inhalt" zu erhalten
if let Some(obj) = manifest.as_object_mut() {
obj.insert(
"signature".to_string(),
serde_json::Value::String("".to_string()),
);
}
// Serialisiere das modifizierte Manifest zurück (mit 2 Spaces, wie in JS)
let canonical_manifest_content = serde_json::to_string_pretty(&manifest).unwrap();
println!("canonical_manifest_content: {}", canonical_manifest_content);
hasher.update(canonical_manifest_content.as_bytes());
} else {
// FÜR ALLE ANDEREN DATEIEN:
let content = fs::read(&file_path)
.map_err(|e| format!("Cannot read file {}: {}", file_path.display(), e))?;
hasher.update(&content); hasher.update(&content);
} else if path.is_dir() {
let subdir_hash = Self::hash_directory(&path)?;
hasher.update(hex::decode(&subdir_hash).unwrap());
} }
} }
Ok(hex::encode(hasher.finalize())) Ok(hex::encode(hasher.finalize()))
} }
fn collect_files_recursively(dir: &Path, file_list: &mut Vec<PathBuf>) -> std::io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
Self::collect_files_recursively(&path, file_list)?;
} else {
file_list.push(path);
}
}
}
Ok(())
}
} }

View File

@ -9,6 +9,7 @@ pub enum ExtensionErrorCode {
SecurityViolation = 1000, SecurityViolation = 1000,
NotFound = 1001, NotFound = 1001,
PermissionDenied = 1002, PermissionDenied = 1002,
MutexPoisoned = 1003,
Database = 2000, Database = 2000,
Filesystem = 2001, Filesystem = 2001,
Http = 2002, Http = 2002,
@ -17,6 +18,7 @@ pub enum ExtensionErrorCode {
Validation = 3001, Validation = 3001,
InvalidPublicKey = 4000, InvalidPublicKey = 4000,
InvalidSignature = 4001, InvalidSignature = 4001,
InvalidActionString = 4004,
SignatureVerificationFailed = 4002, SignatureVerificationFailed = 4002,
CalculateHash = 4003, CalculateHash = 4003,
Installation = 5000, Installation = 5000,
@ -76,6 +78,12 @@ pub enum ExtensionError {
#[error("Invalid Public Key: {reason}")] #[error("Invalid Public Key: {reason}")]
InvalidPublicKey { reason: String }, InvalidPublicKey { reason: String },
#[error("Invalid Action: {input} for resource {resource_type}")]
InvalidActionString {
input: String,
resource_type: String,
},
#[error("Invalid Signature: {reason}")] #[error("Invalid Signature: {reason}")]
InvalidSignature { reason: String }, InvalidSignature { reason: String },
@ -87,6 +95,9 @@ pub enum ExtensionError {
#[error("Extension installation failed: {reason}")] #[error("Extension installation failed: {reason}")]
InstallationFailed { reason: String }, InstallationFailed { reason: String },
#[error("A mutex was poisoned: {reason}")]
MutexPoisoned { reason: String },
} }
impl ExtensionError { impl ExtensionError {
@ -109,6 +120,8 @@ impl ExtensionError {
} }
ExtensionError::InstallationFailed { .. } => ExtensionErrorCode::Installation, ExtensionError::InstallationFailed { .. } => ExtensionErrorCode::Installation,
ExtensionError::CalculateHashError { .. } => ExtensionErrorCode::CalculateHash, ExtensionError::CalculateHashError { .. } => ExtensionErrorCode::CalculateHash,
ExtensionError::MutexPoisoned { .. } => ExtensionErrorCode::MutexPoisoned,
ExtensionError::InvalidActionString { .. } => ExtensionErrorCode::InvalidActionString,
} }
} }

View File

@ -58,7 +58,7 @@ impl FilesystemPath {
/// This would be implemented in your Tauri backend /// This would be implemented in your Tauri backend
pub fn resolve_system_path( pub fn resolve_system_path(
&self, &self,
app_handle: &tauri::AppHandle, _app_handle: &tauri::AppHandle,
) -> Result<String, ExtensionError> { ) -> Result<String, ExtensionError> {
/* let base_dir = match self.path_type { /* let base_dir = match self.path_type {
FilesystemPathType::AppData => app_handle.path().app_data_dir(), FilesystemPathType::AppData => app_handle.path().app_data_dir(),

View File

@ -57,11 +57,11 @@ pub fn get_all_extensions(state: State<AppState>) -> Result<Vec<ExtensionInfoRes
#[tauri::command] #[tauri::command]
pub async fn preview_extension( pub async fn preview_extension(
state: State<'_, AppState>, state: State<'_, AppState>,
source_path: String, extension_path: String,
) -> Result<ExtensionPreview, ExtensionError> { ) -> Result<ExtensionPreview, ExtensionError> {
state state
.extension_manager .extension_manager
.preview_extension_internal(source_path) .preview_extension_internal(extension_path)
.await .await
} }
@ -160,13 +160,20 @@ pub async fn install_extension(
#[tauri::command] #[tauri::command]
pub async fn remove_extension( pub async fn remove_extension(
app_handle: AppHandle, app_handle: AppHandle,
extension_id: String, key_hash: &str,
extension_version: String, extension_id: &str,
extension_version: &str,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), ExtensionError> { ) -> Result<(), ExtensionError> {
state state
.extension_manager .extension_manager
.remove_extension_internal(&app_handle, extension_id, extension_version, &state) .remove_extension_internal(
&app_handle,
key_hash,
extension_id,
extension_version,
&state,
)
.await .await
} }

View File

@ -4,14 +4,10 @@ use crate::database::core::with_connection;
use crate::database::error::DatabaseError; use crate::database::error::DatabaseError;
use crate::extension::database::executor::SqlExecutor; use crate::extension::database::executor::SqlExecutor;
use crate::extension::error::ExtensionError; use crate::extension::error::ExtensionError;
use crate::extension::permissions::types::{parse_constraints, Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints, PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints}; use crate::extension::permissions::types::{Action, ExtensionPermission, PermissionStatus, ResourceType};
use serde_json;
use serde_json::json;
use std::path::Path;
use tauri::State; use tauri::State;
use url::Url; use crate::database::generated::HaexExtensionPermissions;
use crate::database::generated::HaexExtensionPermissions; use rusqlite::params;
use rusqlite::{params, ToSql};
pub struct PermissionManager; pub struct PermissionManager;
@ -19,7 +15,6 @@ impl PermissionManager {
/// Speichert alle Permissions einer Extension /// Speichert alle Permissions einer Extension
pub async fn save_permissions( pub async fn save_permissions(
app_state: &State<'_, AppState>, app_state: &State<'_, AppState>,
extension_id: &str,
permissions: &[ExtensionPermission], permissions: &[ExtensionPermission],
) -> Result<(), ExtensionError> { ) -> Result<(), ExtensionError> {
with_connection(&app_state.db, |conn| { with_connection(&app_state.db, |conn| {
@ -151,17 +146,28 @@ impl PermissionManager {
) -> Result<(), ExtensionError> { ) -> Result<(), ExtensionError> {
with_connection(&app_state.db, |conn| { with_connection(&app_state.db, |conn| {
let tx = conn.transaction().map_err(DatabaseError::from)?; let tx = conn.transaction().map_err(DatabaseError::from)?;
let hlc_service = app_state.hlc.lock() let hlc_service = app_state.hlc.lock()
.map_err(|_| DatabaseError::MutexPoisoned { .map_err(|_| DatabaseError::MutexPoisoned {
reason: "Failed to lock HLC service".to_string(), reason: "Failed to lock HLC service".to_string(),
})?; })?;
let sql = format!("DELETE FROM {} WHERE extension_id = ?", TABLE_EXTENSION_PERMISSIONS); let sql = format!("DELETE FROM {} WHERE extension_id = ?", TABLE_EXTENSION_PERMISSIONS);
SqlExecutor::execute_internal_typed(&tx, &hlc_service, &sql, params![extension_id])?; SqlExecutor::execute_internal_typed(&tx, &hlc_service, &sql, params![extension_id])?;
tx.commit().map_err(DatabaseError::from) tx.commit().map_err(DatabaseError::from)
}).map_err(ExtensionError::from) }).map_err(ExtensionError::from)
} }
/// Löscht alle Permissions einer Extension innerhalb einer bestehenden Transaktion
pub fn delete_permissions_in_transaction(
tx: &rusqlite::Transaction,
hlc_service: &crate::crdt::hlc::HlcService,
extension_id: &str,
) -> Result<(), DatabaseError> {
let sql = format!("DELETE FROM {} WHERE extension_id = ?", TABLE_EXTENSION_PERMISSIONS);
SqlExecutor::execute_internal_typed(tx, hlc_service, &sql, params![extension_id])?;
Ok(())
}
/// Lädt alle Permissions einer Extension /// Lädt alle Permissions einer Extension
pub async fn get_permissions( pub async fn get_permissions(
app_state: &State<'_, AppState>, app_state: &State<'_, AppState>,
@ -184,8 +190,6 @@ impl PermissionManager {
}).map_err(ExtensionError::from) }).map_err(ExtensionError::from)
} }
/// Prüft Datenbankberechtigungen /// Prüft Datenbankberechtigungen
pub async fn check_database_permission( pub async fn check_database_permission(
app_state: &State<'_, AppState>, app_state: &State<'_, AppState>,

View File

@ -1,13 +1,158 @@
// src-tauri/src/extension/permissions/types.rs use crate::extension::error::ExtensionError;
use std::str::FromStr;
use crate::{
database::{error::DatabaseError, generated::HaexExtensionPermissions},
extension::permissions::manager::PermissionManager,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr;
use ts_rs::TS;
// --- Spezifische Aktionen ---
/// Definiert Aktionen, die auf eine Datenbank angewendet werden können.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export)]
pub enum DbAction {
Read,
ReadWrite,
Create,
Delete,
AlterDrop,
}
impl DbAction {
/// Prüft, ob diese Aktion Lesezugriff gewährt (implizites Recht).
pub fn allows_read(&self) -> bool {
matches!(self, DbAction::Read | DbAction::ReadWrite)
}
/// Prüft, ob diese Aktion Schreibzugriff gewährt.
pub fn allows_write(&self) -> bool {
matches!(
self,
DbAction::ReadWrite | DbAction::Create | DbAction::Delete
)
}
}
impl FromStr for DbAction {
type Err = ExtensionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"read" => Ok(DbAction::Read),
"read_write" => Ok(DbAction::ReadWrite),
"create" => Ok(DbAction::Create),
"delete" => Ok(DbAction::Delete),
_ => Err(ExtensionError::InvalidActionString {
input: s.to_string(),
resource_type: "database".to_string(),
}),
}
}
}
/// Definiert Aktionen, die auf das Dateisystem angewendet werden können.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export)]
pub enum FsAction {
Read,
ReadWrite,
}
impl FsAction {
/// Prüft, ob diese Aktion Lesezugriff gewährt (implizites Recht).
pub fn allows_read(&self) -> bool {
matches!(self, FsAction::Read | FsAction::ReadWrite)
}
/// Prüft, ob diese Aktion Schreibzugriff gewährt.
pub fn allows_write(&self) -> bool {
matches!(self, FsAction::ReadWrite)
}
}
impl FromStr for FsAction {
type Err = ExtensionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"read" => Ok(FsAction::Read),
"read_write" => Ok(FsAction::ReadWrite),
_ => Err(ExtensionError::InvalidActionString {
input: s.to_string(),
resource_type: "filesystem".to_string(),
}),
}
}
}
/// Definiert Aktionen (HTTP-Methoden), die auf HTTP-Anfragen angewendet werden können.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "UPPERCASE")]
#[ts(export)]
pub enum HttpAction {
Get,
Post,
Put,
Patch,
Delete,
#[serde(rename = "*")]
All,
}
impl FromStr for HttpAction {
type Err = ExtensionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_uppercase().as_str() {
"GET" => Ok(HttpAction::Get),
"POST" => Ok(HttpAction::Post),
"PUT" => Ok(HttpAction::Put),
"PATCH" => Ok(HttpAction::Patch),
"DELETE" => Ok(HttpAction::Delete),
"*" => Ok(HttpAction::All),
_ => Err(ExtensionError::InvalidActionString {
input: s.to_string(),
resource_type: "http".to_string(),
}),
}
}
}
/// Definiert Aktionen, die auf Shell-Befehle angewendet werden können.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export)]
pub enum ShellAction {
Execute,
}
impl FromStr for ShellAction {
type Err = ExtensionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"execute" => Ok(ShellAction::Execute),
_ => Err(ExtensionError::InvalidActionString {
input: s.to_string(),
resource_type: "shell".to_string(),
}),
}
}
}
// --- Haupt-Typen für Berechtigungen ---
/// Ein typsicherer Container, der die spezifische Aktion für einen Ressourcentyp enthält.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export)]
pub enum Action {
Database(DbAction),
Filesystem(FsAction),
Http(HttpAction),
Shell(ShellAction),
}
/// Die interne Repräsentation einer einzelnen, gewährten Berechtigung.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionPermission { pub struct ExtensionPermission {
pub id: String, pub id: String,
@ -18,62 +163,15 @@ pub struct ExtensionPermission {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub constraints: Option<PermissionConstraints>, pub constraints: Option<PermissionConstraints>,
pub status: PermissionStatus, pub status: PermissionStatus,
// CRDT Felder
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub haex_tombstone: Option<bool>, pub haex_tombstone: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub haex_timestamp: Option<String>, pub haex_timestamp: Option<String>,
} }
impl From<HaexExtensionPermissions> for ExtensionPermission { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)]
fn from(db_perm: HaexExtensionPermissions) -> Self {
let resource_type = ResourceType::from_str(&db_perm.resource_type.unwrap_or_default())
.unwrap_or(ResourceType::Db); // Fallback
let constraints = db_perm
.constraints
.and_then(|json_str| parse_constraints(&resource_type, &json_str).ok());
ExtensionPermission {
id: db_perm.id,
extension_id: db_perm.extension_id.unwrap_or_default(),
resource_type,
action: Action::from_str(&db_perm.action.unwrap_or_default()).unwrap_or(Action::Read),
target: db_perm.target.unwrap_or_default(),
status: PermissionStatus::from_str(&db_perm.status).unwrap_or(PermissionStatus::Ask),
constraints,
haex_timestamp: db_perm.haex_timestamp,
haex_tombstone: db_perm.haex_tombstone,
}
}
}
impl From<&ExtensionPermission> for HaexExtensionPermissions {
fn from(perm: &ExtensionPermission) -> Self {
let constraints_json = perm
.constraints
.as_ref()
.and_then(|c| serde_json::to_string(c).ok());
HaexExtensionPermissions {
id: perm.id.clone(),
extension_id: Some(perm.extension_id.clone()),
resource_type: Some(format!("{:?}", perm.resource_type).to_lowercase()),
action: Some(format!("{:?}", perm.action).to_lowercase()),
target: Some(perm.target.clone()),
constraints: constraints_json,
status: perm.status.as_str().to_string(),
created_at: None, // Wird von der DB gesetzt
updated_at: None, // Wird von der DB gesetzt
haex_timestamp: perm.haex_timestamp.clone(),
haex_tombstone: perm.haex_tombstone,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[ts(export)]
pub enum ResourceType { pub enum ResourceType {
Fs, Fs,
Http, Http,
@ -81,49 +179,140 @@ pub enum ResourceType {
Shell, Shell,
} }
impl FromStr for ResourceType { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)]
type Err = DatabaseError; #[serde(rename_all = "lowercase")]
#[ts(export)]
pub enum PermissionStatus {
Ask,
Granted,
Denied,
}
fn from_str(s: &str) -> Result<Self, Self::Err> { // --- Constraint-Typen (unverändert) ---
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
#[serde(untagged)]
#[ts(export)]
pub enum PermissionConstraints {
Database(DbConstraints),
Filesystem(FsConstraints),
Http(HttpConstraints),
Shell(ShellConstraints),
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
#[ts(export)]
pub struct DbConstraints {
#[serde(skip_serializing_if = "Option::is_none")]
pub where_clause: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub columns: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
#[ts(export)]
pub struct FsConstraints {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_file_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_extensions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recursive: Option<bool>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
#[ts(export)]
pub struct HttpConstraints {
#[serde(skip_serializing_if = "Option::is_none")]
pub methods: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rate_limit: Option<RateLimit>,
}
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
#[ts(export)]
pub struct RateLimit {
pub requests: u32,
pub per_minutes: u32,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
#[ts(export)]
pub struct ShellConstraints {
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_subcommands: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_flags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub forbidden_args: Option<Vec<String>>,
}
// --- Konvertierungen zwischen ExtensionPermission und HaexExtensionPermissions ---
impl ResourceType {
pub fn as_str(&self) -> &str {
match self {
ResourceType::Fs => "fs",
ResourceType::Http => "http",
ResourceType::Db => "db",
ResourceType::Shell => "shell",
}
}
pub fn from_str(s: &str) -> Result<Self, ExtensionError> {
match s { match s {
"fs" => Ok(ResourceType::Fs), "fs" => Ok(ResourceType::Fs),
"http" => Ok(ResourceType::Http), "http" => Ok(ResourceType::Http),
"db" => Ok(ResourceType::Db), "db" => Ok(ResourceType::Db),
"shell" => Ok(ResourceType::Shell), "shell" => Ok(ResourceType::Shell),
_ => Err(DatabaseError::SerializationError { _ => Err(ExtensionError::ValidationError {
reason: format!("Unbekannter Ressourcentyp: {}", s), reason: format!("Unknown resource type: {}", s),
}), }),
} }
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] impl Action {
#[serde(rename_all = "lowercase")] pub fn as_str(&self) -> String {
pub enum Action { match self {
Read, Action::Database(action) => serde_json::to_string(action)
Write, .unwrap_or_default()
} .trim_matches('"')
.to_string(),
impl FromStr for Action { Action::Filesystem(action) => serde_json::to_string(action)
type Err = DatabaseError; .unwrap_or_default()
.trim_matches('"')
fn from_str(s: &str) -> Result<Self, Self::Err> { .to_string(),
match s { Action::Http(action) => serde_json::to_string(action)
"read" => Ok(Action::Read), .unwrap_or_default()
"write" => Ok(Action::Write), .trim_matches('"')
_ => Err(DatabaseError::SerializationError { .to_string(),
reason: format!("Unbekannte Aktion: {}", s), Action::Shell(action) => serde_json::to_string(action)
}), .unwrap_or_default()
.trim_matches('"')
.to_string(),
} }
} }
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub fn from_str(resource_type: &ResourceType, s: &str) -> Result<Self, ExtensionError> {
#[serde(rename_all = "lowercase")] match resource_type {
pub enum PermissionStatus { ResourceType::Db => Ok(Action::Database(DbAction::from_str(s)?)),
Ask, ResourceType::Fs => Ok(Action::Filesystem(FsAction::from_str(s)?)),
Granted, ResourceType::Http => {
Denied, let action: HttpAction =
serde_json::from_str(&format!("\"{}\"", s)).map_err(|_| {
ExtensionError::InvalidActionString {
input: s.to_string(),
resource_type: "http".to_string(),
}
})?;
Ok(Action::Http(action))
}
ResourceType::Shell => Ok(Action::Shell(ShellAction::from_str(s)?)),
}
}
} }
impl PermissionStatus { impl PermissionStatus {
@ -135,140 +324,71 @@ impl PermissionStatus {
} }
} }
pub fn from_str(s: &str) -> Result<Self, DatabaseError> { pub fn from_str(s: &str) -> Result<Self, ExtensionError> {
match s { match s {
"ask" => Ok(PermissionStatus::Ask), "ask" => Ok(PermissionStatus::Ask),
"granted" => Ok(PermissionStatus::Granted), "granted" => Ok(PermissionStatus::Granted),
"denied" => Ok(PermissionStatus::Denied), "denied" => Ok(PermissionStatus::Denied),
_ => Err(DatabaseError::SerializationError { _ => Err(ExtensionError::ValidationError {
reason: format!("Unknown permission status: {}", s), reason: format!("Unknown permission status: {}", s),
}), }),
} }
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug)] impl From<&ExtensionPermission> for crate::database::generated::HaexExtensionPermissions {
#[serde(untagged)] fn from(perm: &ExtensionPermission) -> Self {
pub enum PermissionConstraints { Self {
Database(DbConstraints), id: perm.id.clone(),
Filesystem(FsConstraints), extension_id: Some(perm.extension_id.clone()),
Http(HttpConstraints), resource_type: Some(perm.resource_type.as_str().to_string()),
Shell(ShellConstraints), action: Some(perm.action.as_str()),
} target: Some(perm.target.clone()),
constraints: perm
#[derive(Serialize, Deserialize, Clone, Debug)] .constraints
pub struct DbConstraints { .as_ref()
#[serde(skip_serializing_if = "Option::is_none")] .and_then(|c| serde_json::to_string(c).ok()),
pub where_clause: Option<String>, status: perm.status.as_str().to_string(),
#[serde(skip_serializing_if = "Option::is_none")] created_at: None,
pub columns: Option<Vec<String>>, updated_at: None,
#[serde(skip_serializing_if = "Option::is_none")] haex_tombstone: perm.haex_tombstone,
pub limit: Option<u32>, haex_timestamp: perm.haex_timestamp.clone(),
} }
}
#[derive(Serialize, Deserialize, Clone, Debug)] }
pub struct FsConstraints {
#[serde(skip_serializing_if = "Option::is_none")] impl From<crate::database::generated::HaexExtensionPermissions> for ExtensionPermission {
pub max_file_size: Option<u64>, fn from(db_perm: crate::database::generated::HaexExtensionPermissions) -> Self {
#[serde(skip_serializing_if = "Option::is_none")] let resource_type = db_perm
pub allowed_extensions: Option<Vec<String>>, .resource_type
#[serde(skip_serializing_if = "Option::is_none")] .as_deref()
pub recursive: Option<bool>, .and_then(|s| ResourceType::from_str(s).ok())
} .unwrap_or(ResourceType::Db);
#[derive(Serialize, Deserialize, Clone, Debug)] let action = db_perm
pub struct HttpConstraints { .action
#[serde(skip_serializing_if = "Option::is_none")] .as_deref()
pub methods: Option<Vec<String>>, .and_then(|s| Action::from_str(&resource_type, s).ok())
#[serde(skip_serializing_if = "Option::is_none")] .unwrap_or(Action::Database(DbAction::Read));
pub rate_limit: Option<RateLimit>,
} let status =
PermissionStatus::from_str(db_perm.status.as_str()).unwrap_or(PermissionStatus::Denied);
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RateLimit { let constraints = db_perm
pub requests: u32, .constraints
pub per_minutes: u32, .as_deref()
} .and_then(|s| serde_json::from_str(s).ok());
#[derive(Serialize, Deserialize, Clone, Debug)] Self {
pub struct ShellConstraints { id: db_perm.id,
#[serde(skip_serializing_if = "Option::is_none")] extension_id: db_perm.extension_id.unwrap_or_default(),
pub allowed_subcommands: Option<Vec<String>>, resource_type,
#[serde(skip_serializing_if = "Option::is_none")] action,
pub allowed_flags: Option<Vec<String>>, target: db_perm.target.unwrap_or_default(),
#[serde(skip_serializing_if = "Option::is_none")] constraints,
pub forbidden_args: Option<Vec<String>>, status,
} haex_tombstone: db_perm.haex_tombstone,
haex_timestamp: db_perm.haex_timestamp,
// Wenn du weiterhin gruppierte Permissions brauchst:
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EditablePermissions {
pub permissions: Vec<ExtensionPermission>,
}
// Oder gruppiert nach Typ:
/* impl EditablePermissions {
pub fn database_permissions(&self) -> Vec<&ExtensionPermission> {
self.permissions
.iter()
.filter(|p| p.resource_type == ResourceType::Db)
.collect()
}
pub fn filesystem_permissions(&self) -> Vec<&ExtensionPermission> {
self.permissions
.iter()
.filter(|p| p.resource_type == ResourceType::Fs)
.collect()
}
pub fn http_permissions(&self) -> Vec<&ExtensionPermission> {
self.permissions
.iter()
.filter(|p| p.resource_type == ResourceType::Http)
.collect()
}
pub fn shell_permissions(&self) -> Vec<&ExtensionPermission> {
self.permissions
.iter()
.filter(|p| p.resource_type == ResourceType::Shell)
.collect()
}
} */
pub fn parse_constraints(
resource_type: &ResourceType,
json: &str,
) -> Result<PermissionConstraints, DatabaseError> {
match resource_type {
ResourceType::Db => {
let constraints: DbConstraints =
serde_json::from_str(json).map_err(|e| DatabaseError::SerializationError {
reason: format!("Failed to parse DB constraints: {}", e),
})?;
Ok(PermissionConstraints::Database(constraints))
}
ResourceType::Fs => {
let constraints: FsConstraints =
serde_json::from_str(json).map_err(|e| DatabaseError::SerializationError {
reason: format!("Failed to parse FS constraints: {}", e),
})?;
Ok(PermissionConstraints::Filesystem(constraints))
}
ResourceType::Http => {
let constraints: HttpConstraints =
serde_json::from_str(json).map_err(|e| DatabaseError::SerializationError {
reason: format!("Failed to parse HTTP constraints: {}", e),
})?;
Ok(PermissionConstraints::Http(constraints))
}
ResourceType::Shell => {
let constraints: ShellConstraints =
serde_json::from_str(json).map_err(|e| DatabaseError::SerializationError {
reason: format!("Failed to parse Shell constraints: {}", e),
})?;
Ok(PermissionConstraints::Shell(constraints))
} }
} }
} }

View File

@ -54,7 +54,7 @@ impl SqlPermissionValidator {
PermissionManager::check_database_permission( PermissionManager::check_database_permission(
app_state, app_state,
extension_id, extension_id,
Action::Read, Action::Database(super::types::DbAction::Read),
&table_name, &table_name,
) )
.await?; .await?;
@ -75,7 +75,7 @@ impl SqlPermissionValidator {
PermissionManager::check_database_permission( PermissionManager::check_database_permission(
app_state, app_state,
extension_id, extension_id,
Action::Write, Action::Database(super::types::DbAction::ReadWrite),
&table_name, &table_name,
) )
.await?; .await?;
@ -97,7 +97,7 @@ impl SqlPermissionValidator {
PermissionManager::check_database_permission( PermissionManager::check_database_permission(
app_state, app_state,
extension_id, extension_id,
Action::Write, Action::Database(super::types::DbAction::Create),
&table_name, &table_name,
) )
.await?; .await?;
@ -119,7 +119,7 @@ impl SqlPermissionValidator {
PermissionManager::check_database_permission( PermissionManager::check_database_permission(
app_state, app_state,
extension_id, extension_id,
Action::Write, Action::Database(super::types::DbAction::AlterDrop),
&table_name, &table_name,
) )
.await?; .await?;

View File

@ -1,11 +1,7 @@
mod crdt; mod crdt;
mod database; mod database;
mod extension; mod extension;
use crate::{ use crate::{crdt::hlc::HlcService, database::DbConnection, extension::core::ExtensionManager};
crdt::hlc::HlcService,
database::DbConnection,
extension::core::{ExtensionManager, ExtensionState},
};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tauri::Manager; use tauri::Manager;
@ -60,7 +56,7 @@ pub fn run() {
hlc: Mutex::new(HlcService::new()), hlc: Mutex::new(HlcService::new()),
extension_manager: ExtensionManager::new(), extension_manager: ExtensionManager::new(),
}) })
.manage(ExtensionState::default()) //.manage(ExtensionState::default())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_http::init())

View File

@ -5,166 +5,249 @@
@confirm="onConfirm" @confirm="onConfirm"
> >
<template #title> <template #title>
<i18n-t {{ t('title') }}
keypath="question"
tag="p"
>
<template #extension>
<span class="font-bold text-primary">{{ manifest?.name }}</span>
</template>
</i18n-t>
</template> </template>
<div class="flex flex-col"> <template #body>
<nav <div class="flex flex-col gap-6">
class="tabs tabs-bordered" <!-- Extension Info -->
aria-label="Tabs" <UCard>
role="tablist" <div class="flex items-start gap-4">
aria-orientation="horizontal" <div
> v-if="preview?.manifest.icon"
<button class="w-16 h-16 flex-shrink-0"
v-show="manifest?.permissions?.database" >
id="tabs-basic-item-1" <UIcon
type="button" :name="preview.manifest.icon"
class="tab active-tab:tab-active active" class="w-full h-full"
data-tab="#tabs-basic-1" />
aria-controls="tabs-basic-1" </div>
role="tab" <div class="flex-1">
aria-selected="true" <h3 class="text-xl font-bold">
> {{ preview?.manifest.name }}
{{ t('database') }} </h3>
</button> <p class="text-sm text-gray-500 dark:text-gray-400">
<button {{ t('version') }}: {{ preview?.manifest.version }}
v-show="manifest?.permissions?.filesystem" </p>
id="tabs-basic-item-2" <p
type="button" v-if="preview?.manifest.author"
class="tab active-tab:tab-active" class="text-sm text-gray-500 dark:text-gray-400"
data-tab="#tabs-basic-2" >
aria-controls="tabs-basic-2" {{ t('author') }}: {{ preview.manifest.author }}
role="tab" </p>
aria-selected="false" <p
> v-if="preview?.manifest.description"
{{ t('filesystem') }} class="text-sm mt-2"
</button> >
<button {{ preview.manifest.description }}
v-show="manifest?.permissions?.http" </p>
id="tabs-basic-item-3"
type="button"
class="tab active-tab:tab-active"
data-tab="#tabs-basic-3"
aria-controls="tabs-basic-3"
role="tab"
aria-selected="false"
>
{{ t('http') }}
</button>
</nav>
<div class="mt-3 min-h-40"> <!-- Signature Verification -->
<div <UBadge
id="tabs-basic-1" :color="preview?.is_valid_signature ? 'success' : 'error'"
role="tabpanel" variant="subtle"
aria-labelledby="tabs-basic-item-1" class="mt-2"
> >
<HaexExtensionManifestPermissionsDatabase <template #leading>
:database="permissions?.database" <UIcon
/> :name="
</div> preview?.is_valid_signature
<div ? 'i-heroicons-shield-check'
id="tabs-basic-2" : 'i-heroicons-shield-exclamation'
class="hidden" "
role="tabpanel" />
aria-labelledby="tabs-basic-item-2" </template>
> {{
<HaexExtensionManifestPermissionsFilesystem preview?.is_valid_signature
:filesystem="permissions?.filesystem" ? t('signature.valid')
/> : t('signature.invalid')
</div> }}
<div </UBadge>
id="tabs-basic-3" </div>
class="hidden" </div>
role="tabpanel" </UCard>
aria-labelledby="tabs-basic-item-3"
> <!-- Permissions Section -->
<HaexExtensionManifestPermissionsHttp :http="permissions?.http" /> <div class="flex flex-col gap-4">
<h4 class="text-lg font-semibold">
{{ t('permissions.title') }}
</h4>
<UAccordion
:items="permissionAccordionItems"
:ui="{ root: 'flex flex-col gap-2' }"
>
<template #database>
<div
v-if="databasePermissions"
class="pb-4"
>
<HaexExtensionPermissionList
v-model="databasePermissions"
:title="t('permissions.database')"
/>
</div>
</template>
<template #filesystem>
<div
v-if="filesystemPermissions"
class="pb-4"
>
<HaexExtensionPermissionList
v-model="filesystemPermissions"
:title="t('permissions.filesystem')"
/>
</div>
</template>
<template #http>
<div
v-if="httpPermissions"
class="pb-4"
>
<HaexExtensionPermissionList
v-model="httpPermissions"
:title="t('permissions.http')"
/>
</div>
</template>
<template #shell>
<div
v-if="shellPermissions"
class="pb-4"
>
<HaexExtensionPermissionList
v-model="shellPermissions"
:title="t('permissions.shell')"
/>
</div>
</template>
</UAccordion>
</div> </div>
</div> </div>
</div> </template>
</UiDialogConfirm> </UiDialogConfirm>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { IHaexHubExtensionManifest } from '~/types/haexhub' import type { ExtensionPreview } from '~~/src-tauri/bindings/ExtensionPreview'
const { t } = useI18n() const { t } = useI18n()
const open = defineModel<boolean>('open', { default: false }) const open = defineModel<boolean>('open', { default: false })
const { manifest } = defineProps<{ const props = defineProps<{
manifest?: IHaexHubExtensionManifest | null preview?: ExtensionPreview | null
}>() }>()
const permissions = computed(() => ({ const databasePermissions = ref(
database: { props.preview?.editable_permissions?.database || [],
read: manifest?.permissions.database?.read?.map((read) => ({ )
[read]: true, const filesystemPermissions = ref(
})), props.preview?.editable_permissions?.filesystem || [],
write: manifest?.permissions.database?.read?.map((write) => ({ )
[write]: true, const httpPermissions = ref(props.preview?.editable_permissions?.http || [])
})), const shellPermissions = ref(props.preview?.editable_permissions?.shell || [])
create: manifest?.permissions.database?.read?.map((create) => ({
[create]: true, // Watch for preview changes
})), watch(
() => props.preview,
(newPreview) => {
if (newPreview?.editable_permissions) {
databasePermissions.value = newPreview.editable_permissions.database || []
filesystemPermissions.value =
newPreview.editable_permissions.filesystem || []
httpPermissions.value = newPreview.editable_permissions.http || []
shellPermissions.value = newPreview.editable_permissions.shell || []
}
}, },
{ immediate: true },
)
filesystem: { const permissionAccordionItems = computed(() => {
read: manifest?.permissions.filesystem?.read?.map((read) => ({ const items = []
[read]: true,
})),
write: manifest?.permissions.filesystem?.write?.map((write) => ({
[write]: true,
})),
},
http: manifest?.permissions.http?.map((http) => ({ if (databasePermissions.value?.length) {
[http]: true, items.push({
})), label: t('permissions.database'),
})) icon: 'i-heroicons-circle-stack',
slot: 'database',
defaultOpen: true,
})
}
if (filesystemPermissions.value?.length) {
items.push({
label: t('permissions.filesystem'),
icon: 'i-heroicons-folder',
slot: 'filesystem',
})
}
if (httpPermissions.value?.length) {
items.push({
label: t('permissions.http'),
icon: 'i-heroicons-globe-alt',
slot: 'http',
})
}
if (shellPermissions.value?.length) {
items.push({
label: t('permissions.shell'),
icon: 'i-heroicons-command-line',
slot: 'shell',
})
}
return items
})
watch(permissions, () => console.log('permissions', permissions.value))
const emit = defineEmits(['deny', 'confirm']) const emit = defineEmits(['deny', 'confirm'])
const onDeny = () => { const onDeny = () => {
open.value = false open.value = false
console.log('onDeny open', open.value)
emit('deny') emit('deny')
} }
const onConfirm = () => { const onConfirm = () => {
open.value = false open.value = false
console.log('onConfirm open', open.value) emit('confirm', {
emit('confirm') database: databasePermissions.value,
filesystem: filesystemPermissions.value,
http: httpPermissions.value,
shell: shellPermissions.value,
})
} }
</script> </script>
<i18n lang="json"> <i18n lang="yaml">
{ de:
"de": { title: Erweiterung installieren
"title": "Erweiterung hinzufügen", version: Version
"question": "Erweiterung {extension} hinzufügen?", author: Autor
"confirm": "Bestätigen", signature:
"deny": "Ablehnen", valid: Signatur verifiziert
"database": "Datenbank", invalid: Signatur ungültig
"http": "Internet", permissions:
"filesystem": "Dateisystem" title: Berechtigungen
}, database: Datenbank
"en": { filesystem: Dateisystem
"title": "Confirm Permission", http: Internet
"question": "Add Extension {extension}?", shell: Terminal
"confirm": "Confirm",
"deny": "Deny", en:
"database": "Database", title: Install Extension
"http": "Internet", version: Version
"filesystem": "Filesystem" author: Author
} signature:
} valid: Signature verified
invalid: Invalid signature
permissions:
title: Permissions
database: Database
filesystem: Filesystem
http: Internet
shell: Terminal
</i18n> </i18n>

View File

@ -0,0 +1,128 @@
<template>
<div
v-if="menuEntry"
class="flex items-center justify-between gap-4 p-3 rounded-lg border border-base-300 bg-base-100"
>
<div class="flex-1 min-w-0">
<div class="font-medium truncate">
{{ modelValue.target }}
</div>
<div
v-if="modelValue.operation"
class="text-sm text-gray-500 dark:text-gray-400"
>
{{ t(`operation.${modelValue.operation}`) }}
</div>
</div>
<div class="flex items-center gap-2">
<!-- Status Selector -->
<USelectMenu
v-model="menuEntry"
:items="statusOptions"
value-attribute="value"
class="w-44"
>
<template #leading>
<UIcon
:name="getStatusIcon(menuEntry?.value)"
:class="getStatusColor(menuEntry?.value)"
/>
</template>
<template #item-leading="{ item }">
<UIcon
:name="getStatusIcon(item?.value)"
:class="getStatusColor(item?.value)"
/>
</template>
</USelectMenu>
</div>
</div>
</template>
<script setup lang="ts">
import type { PermissionEntry } from '~~/src-tauri/bindings/PermissionEntry'
import type { PermissionStatus } from '~~/src-tauri/bindings/PermissionStatus'
const permissionEntry = defineModel<PermissionEntry>({ required: true })
const menuEntry = computed({
get: () =>
statusOptions.value.find(
(option) => option.value == permissionEntry.value.status,
),
set(newStatus) {
const status =
statusOptions.value.find((option) => option.value == newStatus?.value)
?.value || 'denied'
if (isPermissionStatus(status)) {
permissionEntry.value.status = status
} else {
permissionEntry.value.status = 'denied'
}
},
})
const { t } = useI18n()
const isPermissionStatus = (value: string): value is PermissionStatus => {
return ['ask', 'granted', 'denied'].includes(value)
}
const statusOptions = computed(() => [
{
value: 'granted',
label: t('status.granted'),
icon: 'i-heroicons-check-circle',
color: 'text-green-500',
},
{
value: 'ask',
label: t('status.ask'),
icon: 'i-heroicons-question-mark-circle',
color: 'text-yellow-500',
},
{
value: 'denied',
label: t('status.denied'),
icon: 'i-heroicons-x-circle',
color: 'text-red-500',
},
])
const getStatusIcon = (status: string) => {
const option = statusOptions.value.find((o) => o.value === status)
return option?.icon || 'i-heroicons-question-mark-circle'
}
const getStatusColor = (status: string) => {
const option = statusOptions.value.find((o) => o.value === status)
return option?.color || 'text-gray-500'
}
</script>
<i18n lang="yaml">
de:
status:
granted: Erlaubt
ask: Nachfragen
denied: Verweigert
operation:
read: Lesen
write: Schreiben
readWrite: Lesen & Schreiben
request: Anfrage
execute: Ausführen
en:
status:
granted: Granted
ask: Ask
denied: Denied
operation:
read: Read
write: Write
readWrite: Read & Write
request: Request
execute: Execute
</i18n>

View File

@ -0,0 +1,30 @@
<template>
<div
v-if="modelValue?.length"
class="flex flex-col gap-2"
>
<h5
v-if="title"
class="text-sm font-semibold text-gray-700 dark:text-gray-300"
>
{{ title }}
</h5>
<div class="flex flex-col gap-2">
<HaexExtensionPermissionItem
v-for="(perm, index) in modelValue"
:key="perm.target"
v-model="modelValue[index]!"
/>
</div>
</div>
</template>
<script setup lang="ts">
import type { PermissionEntry } from '~~/src-tauri/bindings/PermissionEntry'
defineProps<{
title?: string
}>()
const modelValue = defineModel<PermissionEntry[]>({ default: () => [] })
</script>

View File

@ -84,8 +84,6 @@ const filteredSlots = computed(() => {
) )
}) })
watchImmediate(props, () => console.log('props', props))
const { isSmallScreen } = storeToRefs(useUiStore()) const { isSmallScreen } = storeToRefs(useUiStore())
</script> </script>

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="flex flex-col p-4 relative h-full"> <div class="flex flex-col p-4 relative h-full">
<div <!-- <div
v-if="extensionStore.availableExtensions.length" v-if="extensionStore.availableExtensions.length"
class="flex" class="flex"
> >
<UiButton <UiButton
class="fixed top-20 right-4 btn-square btn-primary" class="fixed top-20 right-4"
@click="prepareInstallExtensionAsync" @click="onSelectExtensionAsync"
> >
<Icon <Icon
name="mdi:plus" name="mdi:plus"
@ -20,26 +20,20 @@
:key="_extension.id" :key="_extension.id"
@remove="onShowRemoveDialog(_extension)" @remove="onShowRemoveDialog(_extension)"
/> />
</div> </div> -->
<div {{ preview }}
v-else <div class="h-full w-full">
class="h-full w-full"
>
<Icon
name="my-icon:extensions-overview"
class="size-full md:size-2/3 md:translate-x-1/5 md:translate-y-1/3"
/>
<div class="fixed top-30 right-10"> <div class="fixed top-30 right-10">
<UiButton <UiButton
class="btn-square btn-primary btn-xl btn-gradient rotate-45"
:tooltip="t('extension.add')" :tooltip="t('extension.add')"
@click="prepareInstallExtensionAsync" @click="onSelectExtensionAsync"
square
size="xl"
> >
<Icon <Icon
name="mdi:plus" name="mdi:plus"
size="1.5em" size="1.5em"
class="rotate-45"
/> />
</UiButton> </UiButton>
</div> </div>
@ -53,7 +47,7 @@
<HaexExtensionDialogInstall <HaexExtensionDialogInstall
v-model:open="showConfirmation" v-model:open="showConfirmation"
:manifest="extension.manifest" :preview="preview"
@confirm="addExtensionAsync" @confirm="addExtensionAsync"
/> />
@ -70,6 +64,8 @@ import type {
IHaexHubExtension, IHaexHubExtension,
IHaexHubExtensionManifest, IHaexHubExtensionManifest,
} from '~/types/haexhub' } from '~/types/haexhub'
import { open } from '@tauri-apps/plugin-dialog'
import type { ExtensionPreview } from '~~/src-tauri/bindings/ExtensionPreview'
definePageMeta({ definePageMeta({
name: 'extensionOverview', name: 'extensionOverview',
@ -112,21 +108,27 @@ const extension = reactive<{
const { add } = useToast() const { add } = useToast()
const { addNotificationAsync } = useNotificationStore() const { addNotificationAsync } = useNotificationStore()
const prepareInstallExtensionAsync = async () => { const preview = ref<ExtensionPreview>()
const onSelectExtensionAsync = async () => {
try { try {
const manifest = await loadExtensionManifestAsync() extension.path = await open({ directory: false, recursive: true })
if (!manifest) throw new Error('No valid Manifest found') if (!extension.path) return
extension.manifest = manifest preview.value = await extensionStore.previewManifestAsync(extension.path)
if (!preview.value) return
// Check if already installed
const isAlreadyInstalled = await extensionStore.isExtensionInstalledAsync({ const isAlreadyInstalled = await extensionStore.isExtensionInstalledAsync({
id: manifest.id, id: preview.value.manifest.id,
version: manifest.version, version: preview.value.manifest.version,
}) })
if (isAlreadyInstalled) { if (isAlreadyInstalled) {
openOverwriteDialog.value = true openOverwriteDialog.value = true
} else { } else {
await addExtensionAsync() showConfirmation.value = true
} }
} catch (error) { } catch (error) {
add({ color: 'error', description: JSON.stringify(error) }) add({ color: 'error', description: JSON.stringify(error) })

View File

@ -4,6 +4,7 @@ import type {
IHaexHubExtension, IHaexHubExtension,
IHaexHubExtensionManifest, IHaexHubExtensionManifest,
} from '~/types/haexhub' } from '~/types/haexhub'
import type { ExtensionPreview } from '@bindings/ExtensionPreview'
interface ExtensionInfoResponse { interface ExtensionInfoResponse {
key_hash: string key_hash: string
@ -302,6 +303,14 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
return true return true
} }
const preview = ref<ExtensionPreview>()
const previewManifestAsync = async (extensionPath: string) => {
preview.value = await invoke<ExtensionPreview>('preview_extension', {
extensionPath,
})
return preview.value
}
/* const readManifestFileAsync = async ( /* const readManifestFileAsync = async (
extensionId: string, extensionId: string,
version: string, version: string,
@ -377,6 +386,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
isActive, isActive,
isExtensionInstalledAsync, isExtensionInstalledAsync,
loadExtensionsAsync, loadExtensionsAsync,
previewManifestAsync,
removeExtensionAsync, removeExtensionAsync,
} }
}) })

View File

@ -2,7 +2,7 @@ import { and, eq, or, type SQLWrapper } from 'drizzle-orm'
import { import {
haexNotifications, haexNotifications,
type InsertHaexNotifications, type InsertHaexNotifications,
} from '~~/src-tauri/database/schemas/vault' } from '~~/src-tauri/database/schemas/haex'
import { import {
isPermissionGranted, isPermissionGranted,
requestPermission, requestPermission,
@ -42,19 +42,18 @@ export const useNotificationStore = defineStore('notificationStore', () => {
console.log('readNotificationsAsync', filter) console.log('readNotificationsAsync', filter)
if (filter) { if (filter) {
return await currentVault.value.drizzle return await currentVault.value?.drizzle
.select() .select()
.from(haexNotifications) .from(haexNotifications)
.where(and(...filter)) .where(and(...filter))
} else { } else {
return await currentVault.value.drizzle.select().from(haexNotifications) return await currentVault.value?.drizzle.select().from(haexNotifications)
} }
} }
const syncNotificationsAsync = async () => { const syncNotificationsAsync = async () => {
notifications.value = await readNotificationsAsync([ notifications.value =
eq(haexNotifications.read, false), (await readNotificationsAsync([eq(haexNotifications.read, false)])) ?? []
])
} }
const addNotificationAsync = async ( const addNotificationAsync = async (
@ -75,7 +74,7 @@ export const useNotificationStore = defineStore('notificationStore', () => {
type: notification.type || 'info', type: notification.type || 'info',
} }
await currentVault.value.drizzle await currentVault.value?.drizzle
.insert(haexNotifications) .insert(haexNotifications)
.values(_notification) .values(_notification)
@ -102,7 +101,7 @@ export const useNotificationStore = defineStore('notificationStore', () => {
const filter = notificationIds.map((id) => eq(haexNotifications.id, id)) const filter = notificationIds.map((id) => eq(haexNotifications.id, id))
console.log('deleteNotificationsAsync', notificationIds) console.log('deleteNotificationsAsync', notificationIds)
return currentVault.value.drizzle return currentVault.value?.drizzle
.delete(haexNotifications) .delete(haexNotifications)
.where(or(...filter)) .where(or(...filter))
} }

View File

@ -1,5 +1,11 @@
{ {
// https://nuxt.com/docs/guide/concepts/typescript // https://nuxt.com/docs/guide/concepts/typescript
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@bindings/*": ["./src-tauri/bindings/*"]
}
},
"files": [], "files": [],
"references": [ "references": [
{ {