mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-18 06:50:51 +01:00
Changes: - Added CLAUDE.md with project instructions - Updated extension manifest bindings (TypeScript) - Regenerated database migrations (consolidated into single migration) - Updated haex schema with table name handling - Enhanced extension manager and manifest handling in Rust - Updated extension store in frontend - Updated vault.db 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
215 lines
7.5 KiB
Rust
215 lines
7.5 KiB
Rust
use crate::extension::error::ExtensionError;
|
|
use crate::extension::permissions::types::{
|
|
Action, DbAction, ExtensionPermission, FsAction, HttpAction, PermissionConstraints,
|
|
PermissionStatus, ResourceType, ShellAction,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::str::FromStr;
|
|
use ts_rs::TS;
|
|
|
|
/// 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 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 name: String,
|
|
pub version: String,
|
|
pub author: Option<String>,
|
|
#[serde(default = "default_entry_value")]
|
|
pub entry: Option<String>,
|
|
pub icon: Option<String>,
|
|
pub public_key: String,
|
|
pub signature: String,
|
|
pub permissions: ExtensionPermissions,
|
|
pub homepage: Option<String>,
|
|
pub description: Option<String>,
|
|
#[serde(default)]
|
|
pub single_instance: Option<bool>,
|
|
}
|
|
|
|
fn default_entry_value() -> Option<String> {
|
|
Some("index.html".to_string())
|
|
}
|
|
|
|
impl ExtensionManifest {
|
|
/// Konvertiert die Manifest-Berechtigungen in das bearbeitbare UI-Modell,
|
|
/// indem der Standardstatus `Granted` gesetzt wird.
|
|
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();
|
|
|
|
if let Some(entries) = &self.database {
|
|
for p in entries {
|
|
if let Some(perm) = Self::create_internal(extension_id, ResourceType::Db, p) {
|
|
permissions.push(perm);
|
|
}
|
|
}
|
|
}
|
|
if let Some(entries) = &self.filesystem {
|
|
for p in entries {
|
|
if let Some(perm) = Self::create_internal(extension_id, ResourceType::Fs, p) {
|
|
permissions.push(perm);
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
permissions
|
|
}
|
|
|
|
/// Parst einen einzelnen `PermissionEntry` und wandelt ihn in die interne, typsichere `ExtensionPermission`-Struktur um.
|
|
fn create_internal(
|
|
extension_id: &str,
|
|
resource_type: ResourceType,
|
|
p: &PermissionEntry,
|
|
) -> Option<ExtensionPermission> {
|
|
let operation_str = p.operation.as_deref().unwrap_or_default();
|
|
|
|
let action = match resource_type {
|
|
ResourceType::Db => DbAction::from_str(operation_str).ok().map(Action::Database),
|
|
ResourceType::Fs => FsAction::from_str(operation_str)
|
|
.ok()
|
|
.map(Action::Filesystem),
|
|
ResourceType::Http => HttpAction::from_str(operation_str).ok().map(Action::Http),
|
|
ResourceType::Shell => ShellAction::from_str(operation_str).ok().map(Action::Shell),
|
|
};
|
|
|
|
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,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
|
|
#[ts(export)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExtensionInfoResponse {
|
|
pub id: String,
|
|
pub public_key: String,
|
|
pub name: String,
|
|
pub version: String,
|
|
pub author: Option<String>,
|
|
pub enabled: bool,
|
|
pub description: Option<String>,
|
|
pub homepage: Option<String>,
|
|
pub icon: Option<String>,
|
|
pub entry: Option<String>,
|
|
pub single_instance: Option<bool>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub dev_server_url: Option<String>,
|
|
}
|
|
|
|
impl ExtensionInfoResponse {
|
|
pub fn from_extension(
|
|
extension: &crate::extension::core::types::Extension,
|
|
) -> Result<Self, ExtensionError> {
|
|
use crate::extension::core::types::ExtensionSource;
|
|
|
|
let dev_server_url = match &extension.source {
|
|
ExtensionSource::Development { dev_server_url, .. } => Some(dev_server_url.clone()),
|
|
ExtensionSource::Production { .. } => None,
|
|
};
|
|
|
|
Ok(Self {
|
|
id: extension.id.clone(),
|
|
public_key: extension.manifest.public_key.clone(),
|
|
name: extension.manifest.name.clone(),
|
|
version: extension.manifest.version.clone(),
|
|
author: extension.manifest.author.clone(),
|
|
enabled: extension.enabled,
|
|
description: extension.manifest.description.clone(),
|
|
homepage: extension.manifest.homepage.clone(),
|
|
icon: extension.manifest.icon.clone(),
|
|
entry: extension.manifest.entry.clone(),
|
|
single_instance: extension.manifest.single_instance,
|
|
dev_server_url,
|
|
})
|
|
}
|
|
}
|