mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 22:20:51 +01:00
refactored install dialog
This commit is contained in:
@ -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',
|
||||||
|
|||||||
15
package.json
15
package.json
@ -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
4
src-tauri/Cargo.lock
generated
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
10
src-tauri/bindings/Action.ts
Normal file
10
src-tauri/bindings/Action.ts
Normal 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 };
|
||||||
3
src-tauri/bindings/DatabaseError.ts
Normal file
3
src-tauri/bindings/DatabaseError.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type 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 };
|
||||||
6
src-tauri/bindings/DbAction.ts
Normal file
6
src-tauri/bindings/DbAction.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definiert Aktionen, die auf eine Datenbank angewendet werden können.
|
||||||
|
*/
|
||||||
|
export type DbAction = "read" | "readwrite" | "create" | "delete" | "alterdrop";
|
||||||
3
src-tauri/bindings/DbConstraints.ts
Normal file
3
src-tauri/bindings/DbConstraints.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type DbConstraints = { where_clause: string | null, columns: Array<string> | null, limit: number | null, };
|
||||||
3
src-tauri/bindings/ExtensionInfoResponse.ts
Normal file
3
src-tauri/bindings/ExtensionInfoResponse.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type ExtensionInfoResponse = { key_hash: string, name: string, full_id: string, version: string, display_name: string | null, namespace: string | null, allowed_origin: string, };
|
||||||
4
src-tauri/bindings/ExtensionManifest.ts
Normal file
4
src-tauri/bindings/ExtensionManifest.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { 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, };
|
||||||
7
src-tauri/bindings/ExtensionPermissions.ts
Normal file
7
src-tauri/bindings/ExtensionPermissions.ts
Normal 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, };
|
||||||
5
src-tauri/bindings/ExtensionPreview.ts
Normal file
5
src-tauri/bindings/ExtensionPreview.ts
Normal 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, };
|
||||||
6
src-tauri/bindings/FsAction.ts
Normal file
6
src-tauri/bindings/FsAction.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definiert Aktionen, die auf das Dateisystem angewendet werden können.
|
||||||
|
*/
|
||||||
|
export type FsAction = "read" | "readwrite";
|
||||||
3
src-tauri/bindings/FsConstraints.ts
Normal file
3
src-tauri/bindings/FsConstraints.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type FsConstraints = { max_file_size: bigint | null, allowed_extensions: Array<string> | null, recursive: boolean | null, };
|
||||||
6
src-tauri/bindings/HttpAction.ts
Normal file
6
src-tauri/bindings/HttpAction.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definiert Aktionen (HTTP-Methoden), die auf HTTP-Anfragen angewendet werden können.
|
||||||
|
*/
|
||||||
|
export type HttpAction = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "*";
|
||||||
4
src-tauri/bindings/HttpConstraints.ts
Normal file
4
src-tauri/bindings/HttpConstraints.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { RateLimit } from "./RateLimit";
|
||||||
|
|
||||||
|
export type HttpConstraints = { methods: Array<string> | null, rate_limit: RateLimit | null, };
|
||||||
7
src-tauri/bindings/PermissionConstraints.ts
Normal file
7
src-tauri/bindings/PermissionConstraints.ts
Normal 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;
|
||||||
19
src-tauri/bindings/PermissionEntry.ts
Normal file
19
src-tauri/bindings/PermissionEntry.ts
Normal 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, };
|
||||||
3
src-tauri/bindings/PermissionStatus.ts
Normal file
3
src-tauri/bindings/PermissionStatus.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type PermissionStatus = "ask" | "granted" | "denied";
|
||||||
3
src-tauri/bindings/RateLimit.ts
Normal file
3
src-tauri/bindings/RateLimit.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type RateLimit = { requests: number, per_minutes: number, };
|
||||||
3
src-tauri/bindings/ResourceType.ts
Normal file
3
src-tauri/bindings/ResourceType.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type ResourceType = "fs" | "http" | "db" | "shell";
|
||||||
6
src-tauri/bindings/ShellAction.ts
Normal file
6
src-tauri/bindings/ShellAction.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definiert Aktionen, die auf Shell-Befehle angewendet werden können.
|
||||||
|
*/
|
||||||
|
export type ShellAction = "execute";
|
||||||
3
src-tauri/bindings/ShellConstraints.ts
Normal file
3
src-tauri/bindings/ShellConstraints.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type ShellConstraints = { allowed_subcommands: Array<string> | null, allowed_flags: Array<string> | null, forbidden_args: Array<string> | null, };
|
||||||
3
src-tauri/bindings/VaultInfo.ts
Normal file
3
src-tauri/bindings/VaultInfo.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type VaultInfo = { name: string, lastAccess: bigint, path: string, };
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>,
|
||||||
|
|||||||
@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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?;
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
128
src/components/haex/extension/permission-item.vue
Normal file
128
src/components/haex/extension/permission-item.vue
Normal 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>
|
||||||
30
src/components/haex/extension/permission-list.vue
Normal file
30
src/components/haex/extension/permission-list.vue
Normal 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>
|
||||||
@ -84,8 +84,6 @@ const filteredSlots = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
watchImmediate(props, () => console.log('props', props))
|
|
||||||
|
|
||||||
const { isSmallScreen } = storeToRefs(useUiStore())
|
const { isSmallScreen } = storeToRefs(useUiStore())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -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) })
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user