mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-18 06:50:51 +01:00
Compare commits
6 Commits
v0.1.13
...
9583e2f44b
| Author | SHA1 | Date | |
|---|---|---|---|
| 9583e2f44b | |||
| d886fbd8bd | |||
| 9bad4008f2 | |||
| 203f81e775 | |||
| 554cb7762d | |||
| 5856a73e5b |
26
README.md
26
README.md
@ -168,6 +168,32 @@ pnpm install
|
|||||||
pnpm tauri dev
|
pnpm tauri dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 📦 Release Process
|
||||||
|
|
||||||
|
Create a new release using the automated scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Patch release (0.1.13 → 0.1.14)
|
||||||
|
pnpm release:patch
|
||||||
|
|
||||||
|
# Minor release (0.1.13 → 0.2.0)
|
||||||
|
pnpm release:minor
|
||||||
|
|
||||||
|
# Major release (0.1.13 → 1.0.0)
|
||||||
|
pnpm release:major
|
||||||
|
```
|
||||||
|
|
||||||
|
The script automatically:
|
||||||
|
1. Updates version in `package.json`
|
||||||
|
2. Creates a git commit
|
||||||
|
3. Creates a git tag
|
||||||
|
4. Pushes to remote
|
||||||
|
|
||||||
|
GitHub Actions will then automatically:
|
||||||
|
- Build desktop apps (macOS, Linux, Windows)
|
||||||
|
- Build Android apps (APK and AAB)
|
||||||
|
- Create and publish a GitHub release
|
||||||
|
|
||||||
#### 🧭 Summary
|
#### 🧭 Summary
|
||||||
|
|
||||||
HaexHub aims to:
|
HaexHub aims to:
|
||||||
|
|||||||
@ -14,6 +14,9 @@
|
|||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
|
"release:patch": "node scripts/release.js patch",
|
||||||
|
"release:minor": "node scripts/release.js minor",
|
||||||
|
"release:major": "node scripts/release.js major",
|
||||||
"tauri:build:debug": "tauri build --debug",
|
"tauri:build:debug": "tauri build --debug",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
|
|||||||
91
scripts/release.js
Executable file
91
scripts/release.js
Executable file
@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const rootDir = join(__dirname, '..');
|
||||||
|
|
||||||
|
const versionType = process.argv[2];
|
||||||
|
|
||||||
|
if (!['patch', 'minor', 'major'].includes(versionType)) {
|
||||||
|
console.error('Usage: pnpm release <patch|minor|major>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current package.json
|
||||||
|
const packageJsonPath = join(rootDir, 'package.json');
|
||||||
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
const currentVersion = packageJson.version;
|
||||||
|
|
||||||
|
if (!currentVersion) {
|
||||||
|
console.error('No version found in package.json');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse version
|
||||||
|
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||||
|
|
||||||
|
// Calculate new version
|
||||||
|
let newVersion;
|
||||||
|
switch (versionType) {
|
||||||
|
case 'major':
|
||||||
|
newVersion = `${major + 1}.0.0`;
|
||||||
|
break;
|
||||||
|
case 'minor':
|
||||||
|
newVersion = `${major}.${minor + 1}.0`;
|
||||||
|
break;
|
||||||
|
case 'patch':
|
||||||
|
newVersion = `${major}.${minor}.${patch + 1}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📦 Bumping version from ${currentVersion} to ${newVersion}`);
|
||||||
|
|
||||||
|
// Update package.json
|
||||||
|
packageJson.version = newVersion;
|
||||||
|
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||||
|
console.log('✅ Updated package.json');
|
||||||
|
|
||||||
|
// Git operations
|
||||||
|
try {
|
||||||
|
// Check if there are uncommitted changes
|
||||||
|
const status = execSync('git status --porcelain', { encoding: 'utf8' });
|
||||||
|
const hasOtherChanges = status
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => line && !line.includes('package.json'))
|
||||||
|
.length > 0;
|
||||||
|
|
||||||
|
if (hasOtherChanges) {
|
||||||
|
console.error('❌ There are uncommitted changes besides package.json. Please commit or stash them first.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add and commit package.json
|
||||||
|
execSync('git add package.json', { stdio: 'inherit' });
|
||||||
|
execSync(`git commit -m "Bump version to ${newVersion}"`, { stdio: 'inherit' });
|
||||||
|
console.log('✅ Committed version bump');
|
||||||
|
|
||||||
|
// Create tag
|
||||||
|
execSync(`git tag v${newVersion}`, { stdio: 'inherit' });
|
||||||
|
console.log(`✅ Created tag v${newVersion}`);
|
||||||
|
|
||||||
|
// Push changes and tag
|
||||||
|
console.log('📤 Pushing to remote...');
|
||||||
|
execSync('git push', { stdio: 'inherit' });
|
||||||
|
execSync(`git push origin v${newVersion}`, { stdio: 'inherit' });
|
||||||
|
console.log('✅ Pushed changes and tag');
|
||||||
|
|
||||||
|
console.log('\n🎉 Release v' + newVersion + ' created successfully!');
|
||||||
|
console.log('📋 GitHub Actions will now build and publish the release.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Git operation failed:', error.message);
|
||||||
|
// Rollback package.json changes
|
||||||
|
packageJson.version = currentVersion;
|
||||||
|
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||||
|
console.log('↩️ Rolled back package.json changes');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use crate::extension::error::ExtensionError;
|
use crate::extension::error::ExtensionError;
|
||||||
use crate::extension::permissions::types::{
|
use crate::extension::permissions::types::{
|
||||||
Action, DbAction, ExtensionPermission, FsAction, HttpAction, PermissionConstraints,
|
Action, DbAction, ExtensionPermission, FsAction, WebAction, PermissionConstraints,
|
||||||
PermissionStatus, ResourceType, ShellAction,
|
PermissionStatus, ResourceType, ShellAction,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -117,7 +117,7 @@ impl ExtensionPermissions {
|
|||||||
}
|
}
|
||||||
if let Some(entries) = &self.http {
|
if let Some(entries) = &self.http {
|
||||||
for p in entries {
|
for p in entries {
|
||||||
if let Some(perm) = Self::create_internal(extension_id, ResourceType::Http, p) {
|
if let Some(perm) = Self::create_internal(extension_id, ResourceType::Web, p) {
|
||||||
permissions.push(perm);
|
permissions.push(perm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,7 +146,7 @@ impl ExtensionPermissions {
|
|||||||
ResourceType::Fs => FsAction::from_str(operation_str)
|
ResourceType::Fs => FsAction::from_str(operation_str)
|
||||||
.ok()
|
.ok()
|
||||||
.map(Action::Filesystem),
|
.map(Action::Filesystem),
|
||||||
ResourceType::Http => HttpAction::from_str(operation_str).ok().map(Action::Http),
|
ResourceType::Web => WebAction::from_str(operation_str).ok().map(Action::Web),
|
||||||
ResourceType::Shell => ShellAction::from_str(operation_str).ok().map(Action::Shell),
|
ResourceType::Shell => ShellAction::from_str(operation_str).ok().map(Action::Shell),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ pub enum ExtensionErrorCode {
|
|||||||
Filesystem = 2001,
|
Filesystem = 2001,
|
||||||
FilesystemWithPath = 2004,
|
FilesystemWithPath = 2004,
|
||||||
Http = 2002,
|
Http = 2002,
|
||||||
|
Web = 2005,
|
||||||
Shell = 2003,
|
Shell = 2003,
|
||||||
Manifest = 3000,
|
Manifest = 3000,
|
||||||
Validation = 3001,
|
Validation = 3001,
|
||||||
@ -83,6 +84,9 @@ pub enum ExtensionError {
|
|||||||
#[error("HTTP request failed: {reason}")]
|
#[error("HTTP request failed: {reason}")]
|
||||||
Http { reason: String },
|
Http { reason: String },
|
||||||
|
|
||||||
|
#[error("Web request failed: {reason}")]
|
||||||
|
WebError { reason: String },
|
||||||
|
|
||||||
#[error("Shell command failed: {reason}")]
|
#[error("Shell command failed: {reason}")]
|
||||||
Shell {
|
Shell {
|
||||||
reason: String,
|
reason: String,
|
||||||
@ -131,6 +135,7 @@ impl ExtensionError {
|
|||||||
ExtensionError::Filesystem { .. } => ExtensionErrorCode::Filesystem,
|
ExtensionError::Filesystem { .. } => ExtensionErrorCode::Filesystem,
|
||||||
ExtensionError::FilesystemWithPath { .. } => ExtensionErrorCode::FilesystemWithPath,
|
ExtensionError::FilesystemWithPath { .. } => ExtensionErrorCode::FilesystemWithPath,
|
||||||
ExtensionError::Http { .. } => ExtensionErrorCode::Http,
|
ExtensionError::Http { .. } => ExtensionErrorCode::Http,
|
||||||
|
ExtensionError::WebError { .. } => ExtensionErrorCode::Web,
|
||||||
ExtensionError::Shell { .. } => ExtensionErrorCode::Shell,
|
ExtensionError::Shell { .. } => ExtensionErrorCode::Shell,
|
||||||
ExtensionError::ManifestError { .. } => ExtensionErrorCode::Manifest,
|
ExtensionError::ManifestError { .. } => ExtensionErrorCode::Manifest,
|
||||||
ExtensionError::ValidationError { .. } => ExtensionErrorCode::Validation,
|
ExtensionError::ValidationError { .. } => ExtensionErrorCode::Validation,
|
||||||
|
|||||||
@ -13,6 +13,7 @@ pub mod database;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod filesystem;
|
pub mod filesystem;
|
||||||
pub mod permissions;
|
pub mod permissions;
|
||||||
|
pub mod web;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_extension_info(
|
pub fn get_extension_info(
|
||||||
|
|||||||
@ -4,7 +4,7 @@ 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::{Action, ExtensionPermission, PermissionStatus, ResourceType};
|
use crate::extension::permissions::types::{Action, ExtensionPermission, PermissionConstraints, PermissionStatus, ResourceType};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use crate::database::generated::HaexExtensionPermissions;
|
use crate::database::generated::HaexExtensionPermissions;
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
@ -245,6 +245,74 @@ impl PermissionManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prüft Web-Berechtigungen für Requests
|
||||||
|
pub async fn check_web_permission(
|
||||||
|
app_state: &State<'_, AppState>,
|
||||||
|
extension_id: &str,
|
||||||
|
method: &str,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
// Optimiert: Lade nur Web-Permissions aus der Datenbank
|
||||||
|
let permissions = with_connection(&app_state.db, |conn| {
|
||||||
|
let sql = format!(
|
||||||
|
"SELECT * FROM {TABLE_EXTENSION_PERMISSIONS} WHERE extension_id = ? AND resource_type = 'web'"
|
||||||
|
);
|
||||||
|
let mut stmt = conn.prepare(&sql).map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
|
let perms_iter = stmt.query_map(params![extension_id], |row| {
|
||||||
|
crate::database::generated::HaexExtensionPermissions::from_row(row)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let permissions: Vec<ExtensionPermission> = perms_iter
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.map(Into::into)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(permissions)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let url_parsed = url::Url::parse(url).map_err(|e| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Invalid URL: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let domain = url_parsed.host_str().ok_or_else(|| ExtensionError::ValidationError {
|
||||||
|
reason: "URL does not contain a valid host".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let has_permission = permissions
|
||||||
|
.iter()
|
||||||
|
.filter(|perm| perm.status == PermissionStatus::Granted)
|
||||||
|
.any(|perm| {
|
||||||
|
let domain_matches = perm.target == "*"
|
||||||
|
|| perm.target == domain
|
||||||
|
|| domain.ends_with(&format!(".{}", perm.target));
|
||||||
|
|
||||||
|
if !domain_matches {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(PermissionConstraints::Web(constraints)) = &perm.constraints {
|
||||||
|
if let Some(methods) = &constraints.methods {
|
||||||
|
if !methods.iter().any(|m| m.eq_ignore_ascii_case(method)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
});
|
||||||
|
|
||||||
|
if !has_permission {
|
||||||
|
return Err(ExtensionError::permission_denied(
|
||||||
|
extension_id,
|
||||||
|
method,
|
||||||
|
&format!("web request to '{}'", url),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/* /// Prüft Dateisystem-Berechtigungen
|
/* /// Prüft Dateisystem-Berechtigungen
|
||||||
pub async fn check_filesystem_permission(
|
pub async fn check_filesystem_permission(
|
||||||
app_state: &State<'_, AppState>,
|
app_state: &State<'_, AppState>,
|
||||||
@ -293,56 +361,6 @@ impl PermissionManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prüft HTTP-Berechtigungen
|
|
||||||
pub async fn check_http_permission(
|
|
||||||
app_state: &State<'_, AppState>,
|
|
||||||
extension_id: &str,
|
|
||||||
method: &str,
|
|
||||||
url: &str,
|
|
||||||
) -> Result<(), ExtensionError> {
|
|
||||||
let permissions = Self::get_permissions(app_state, extension_id).await?;
|
|
||||||
|
|
||||||
let url_parsed = Url::parse(url).map_err(|e| ExtensionError::ValidationError {
|
|
||||||
reason: format!("Invalid URL: {}", e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let domain = url_parsed.host_str().unwrap_or("");
|
|
||||||
|
|
||||||
let has_permission = permissions
|
|
||||||
.iter()
|
|
||||||
.filter(|perm| perm.status == PermissionStatus::Granted)
|
|
||||||
.filter(|perm| perm.resource_type == ResourceType::Http)
|
|
||||||
.any(|perm| {
|
|
||||||
let domain_matches = perm.target == "*"
|
|
||||||
|| perm.target == domain
|
|
||||||
|| domain.ends_with(&format!(".{}", perm.target));
|
|
||||||
|
|
||||||
if !domain_matches {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(PermissionConstraints::Http(constraints)) = &perm.constraints {
|
|
||||||
if let Some(methods) = &constraints.methods {
|
|
||||||
if !methods.iter().any(|m| m.eq_ignore_ascii_case(method)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
});
|
|
||||||
|
|
||||||
if !has_permission {
|
|
||||||
return Err(ExtensionError::permission_denied(
|
|
||||||
extension_id,
|
|
||||||
method,
|
|
||||||
&format!("HTTP request to '{}'", url),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prüft Shell-Berechtigungen
|
/// Prüft Shell-Berechtigungen
|
||||||
pub async fn check_shell_permission(
|
pub async fn check_shell_permission(
|
||||||
app_state: &State<'_, AppState>,
|
app_state: &State<'_, AppState>,
|
||||||
@ -410,7 +428,7 @@ impl PermissionManager {
|
|||||||
pub fn parse_resource_type(s: &str) -> Result<ResourceType, DatabaseError> {
|
pub fn parse_resource_type(s: &str) -> Result<ResourceType, DatabaseError> {
|
||||||
match s {
|
match s {
|
||||||
"fs" => Ok(ResourceType::Fs),
|
"fs" => Ok(ResourceType::Fs),
|
||||||
"http" => Ok(ResourceType::Http),
|
"web" => Ok(ResourceType::Web),
|
||||||
"db" => Ok(ResourceType::Db),
|
"db" => Ok(ResourceType::Db),
|
||||||
"shell" => Ok(ResourceType::Shell),
|
"shell" => Ok(ResourceType::Shell),
|
||||||
_ => Err(DatabaseError::SerializationError {
|
_ => Err(DatabaseError::SerializationError {
|
||||||
|
|||||||
@ -86,11 +86,11 @@ impl FromStr for FsAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Definiert Aktionen (HTTP-Methoden), die auf HTTP-Anfragen angewendet werden können.
|
/// Definiert Aktionen (HTTP-Methoden), die auf Web-Anfragen angewendet werden können.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||||
#[serde(rename_all = "UPPERCASE")]
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub enum HttpAction {
|
pub enum WebAction {
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
@ -100,20 +100,20 @@ pub enum HttpAction {
|
|||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for HttpAction {
|
impl FromStr for WebAction {
|
||||||
type Err = ExtensionError;
|
type Err = ExtensionError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.to_uppercase().as_str() {
|
match s.to_uppercase().as_str() {
|
||||||
"GET" => Ok(HttpAction::Get),
|
"GET" => Ok(WebAction::Get),
|
||||||
"POST" => Ok(HttpAction::Post),
|
"POST" => Ok(WebAction::Post),
|
||||||
"PUT" => Ok(HttpAction::Put),
|
"PUT" => Ok(WebAction::Put),
|
||||||
"PATCH" => Ok(HttpAction::Patch),
|
"PATCH" => Ok(WebAction::Patch),
|
||||||
"DELETE" => Ok(HttpAction::Delete),
|
"DELETE" => Ok(WebAction::Delete),
|
||||||
"*" => Ok(HttpAction::All),
|
"*" => Ok(WebAction::All),
|
||||||
_ => Err(ExtensionError::InvalidActionString {
|
_ => Err(ExtensionError::InvalidActionString {
|
||||||
input: s.to_string(),
|
input: s.to_string(),
|
||||||
resource_type: "http".to_string(),
|
resource_type: "web".to_string(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,7 +149,7 @@ impl FromStr for ShellAction {
|
|||||||
pub enum Action {
|
pub enum Action {
|
||||||
Database(DbAction),
|
Database(DbAction),
|
||||||
Filesystem(FsAction),
|
Filesystem(FsAction),
|
||||||
Http(HttpAction),
|
Web(WebAction),
|
||||||
Shell(ShellAction),
|
Shell(ShellAction),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +173,7 @@ pub struct ExtensionPermission {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub enum ResourceType {
|
pub enum ResourceType {
|
||||||
Fs,
|
Fs,
|
||||||
Http,
|
Web,
|
||||||
Db,
|
Db,
|
||||||
Shell,
|
Shell,
|
||||||
}
|
}
|
||||||
@ -195,7 +195,7 @@ pub enum PermissionStatus {
|
|||||||
pub enum PermissionConstraints {
|
pub enum PermissionConstraints {
|
||||||
Database(DbConstraints),
|
Database(DbConstraints),
|
||||||
Filesystem(FsConstraints),
|
Filesystem(FsConstraints),
|
||||||
Http(HttpConstraints),
|
Web(WebConstraints),
|
||||||
Shell(ShellConstraints),
|
Shell(ShellConstraints),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,7 +223,7 @@ pub struct FsConstraints {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct HttpConstraints {
|
pub struct WebConstraints {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub methods: Option<Vec<String>>,
|
pub methods: Option<Vec<String>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@ -254,7 +254,7 @@ impl ResourceType {
|
|||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
ResourceType::Fs => "fs",
|
ResourceType::Fs => "fs",
|
||||||
ResourceType::Http => "http",
|
ResourceType::Web => "web",
|
||||||
ResourceType::Db => "db",
|
ResourceType::Db => "db",
|
||||||
ResourceType::Shell => "shell",
|
ResourceType::Shell => "shell",
|
||||||
}
|
}
|
||||||
@ -263,7 +263,7 @@ impl ResourceType {
|
|||||||
pub fn from_str(s: &str) -> Result<Self, ExtensionError> {
|
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),
|
"web" => Ok(ResourceType::Web),
|
||||||
"db" => Ok(ResourceType::Db),
|
"db" => Ok(ResourceType::Db),
|
||||||
"shell" => Ok(ResourceType::Shell),
|
"shell" => Ok(ResourceType::Shell),
|
||||||
_ => Err(ExtensionError::ValidationError {
|
_ => Err(ExtensionError::ValidationError {
|
||||||
@ -284,7 +284,7 @@ impl Action {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.trim_matches('"')
|
.trim_matches('"')
|
||||||
.to_string(),
|
.to_string(),
|
||||||
Action::Http(action) => serde_json::to_string(action)
|
Action::Web(action) => serde_json::to_string(action)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.trim_matches('"')
|
.trim_matches('"')
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@ -299,15 +299,15 @@ impl Action {
|
|||||||
match resource_type {
|
match resource_type {
|
||||||
ResourceType::Db => Ok(Action::Database(DbAction::from_str(s)?)),
|
ResourceType::Db => Ok(Action::Database(DbAction::from_str(s)?)),
|
||||||
ResourceType::Fs => Ok(Action::Filesystem(FsAction::from_str(s)?)),
|
ResourceType::Fs => Ok(Action::Filesystem(FsAction::from_str(s)?)),
|
||||||
ResourceType::Http => {
|
ResourceType::Web => {
|
||||||
let action: HttpAction =
|
let action: WebAction =
|
||||||
serde_json::from_str(&format!("\"{s}\"")).map_err(|_| {
|
serde_json::from_str(&format!("\"{s}\"")).map_err(|_| {
|
||||||
ExtensionError::InvalidActionString {
|
ExtensionError::InvalidActionString {
|
||||||
input: s.to_string(),
|
input: s.to_string(),
|
||||||
resource_type: "http".to_string(),
|
resource_type: "web".to_string(),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
Ok(Action::Http(action))
|
Ok(Action::Web(action))
|
||||||
}
|
}
|
||||||
ResourceType::Shell => Ok(Action::Shell(ShellAction::from_str(s)?)),
|
ResourceType::Shell => Ok(Action::Shell(ShellAction::from_str(s)?)),
|
||||||
}
|
}
|
||||||
|
|||||||
210
src-tauri/src/extension/web/mod.rs
Normal file
210
src-tauri/src/extension/web/mod.rs
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
// src-tauri/src/extension/web/mod.rs
|
||||||
|
|
||||||
|
use crate::extension::error::ExtensionError;
|
||||||
|
use crate::AppState;
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tauri::State;
|
||||||
|
use tauri_plugin_http::reqwest;
|
||||||
|
|
||||||
|
/// Request structure matching the SDK's WebRequestOptions
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct WebFetchRequest {
|
||||||
|
pub url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub method: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub headers: Option<HashMap<String, String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub body: Option<String>, // Base64 encoded
|
||||||
|
#[serde(default)]
|
||||||
|
pub timeout: Option<u64>, // milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response structure matching the SDK's WebResponse
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct WebFetchResponse {
|
||||||
|
pub status: u16,
|
||||||
|
pub status_text: String,
|
||||||
|
pub headers: HashMap<String, String>,
|
||||||
|
pub body: String, // Base64 encoded
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn extension_web_open(
|
||||||
|
url: String,
|
||||||
|
public_key: String,
|
||||||
|
name: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
// Get extension to validate it exists
|
||||||
|
let extension = state
|
||||||
|
.extension_manager
|
||||||
|
.get_extension_by_public_key_and_name(&public_key, &name)?
|
||||||
|
.ok_or_else(|| ExtensionError::NotFound {
|
||||||
|
public_key: public_key.clone(),
|
||||||
|
name: name.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Validate URL format
|
||||||
|
let parsed_url = url::Url::parse(&url).map_err(|e| ExtensionError::WebError {
|
||||||
|
reason: format!("Invalid URL: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Only allow http and https URLs
|
||||||
|
let scheme = parsed_url.scheme();
|
||||||
|
if scheme != "http" && scheme != "https" {
|
||||||
|
return Err(ExtensionError::WebError {
|
||||||
|
reason: format!("Unsupported URL scheme: {}. Only http and https are allowed.", scheme),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check web permissions (open uses GET method for permission check)
|
||||||
|
crate::extension::permissions::manager::PermissionManager::check_web_permission(
|
||||||
|
&state,
|
||||||
|
&extension.id,
|
||||||
|
"GET",
|
||||||
|
&url,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Open URL in default browser using tauri-plugin-opener
|
||||||
|
tauri_plugin_opener::open_url(&url, None::<&str>).map_err(|e| ExtensionError::WebError {
|
||||||
|
reason: format!("Failed to open URL in browser: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn extension_web_fetch(
|
||||||
|
url: String,
|
||||||
|
method: Option<String>,
|
||||||
|
headers: Option<HashMap<String, String>>,
|
||||||
|
body: Option<String>,
|
||||||
|
timeout: Option<u64>,
|
||||||
|
public_key: String,
|
||||||
|
name: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<WebFetchResponse, ExtensionError> {
|
||||||
|
// Get extension to validate it exists
|
||||||
|
let extension = state
|
||||||
|
.extension_manager
|
||||||
|
.get_extension_by_public_key_and_name(&public_key, &name)?
|
||||||
|
.ok_or_else(|| ExtensionError::NotFound {
|
||||||
|
public_key: public_key.clone(),
|
||||||
|
name: name.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let method_str = method.as_deref().unwrap_or("GET");
|
||||||
|
|
||||||
|
// Check web permissions before making request
|
||||||
|
crate::extension::permissions::manager::PermissionManager::check_web_permission(
|
||||||
|
&state,
|
||||||
|
&extension.id,
|
||||||
|
method_str,
|
||||||
|
&url,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let request = WebFetchRequest {
|
||||||
|
url,
|
||||||
|
method: Some(method_str.to_string()),
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch_web_request(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs the actual HTTP request without CORS restrictions
|
||||||
|
async fn fetch_web_request(request: WebFetchRequest) -> Result<WebFetchResponse, ExtensionError> {
|
||||||
|
let method_str = request.method.as_deref().unwrap_or("GET");
|
||||||
|
let timeout_ms = request.timeout.unwrap_or(30000);
|
||||||
|
|
||||||
|
// Build reqwest client with timeout
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_millis(timeout_ms))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| ExtensionError::WebError {
|
||||||
|
reason: format!("Failed to create HTTP client: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Build request
|
||||||
|
let mut req_builder = match method_str.to_uppercase().as_str() {
|
||||||
|
"GET" => client.get(&request.url),
|
||||||
|
"POST" => client.post(&request.url),
|
||||||
|
"PUT" => client.put(&request.url),
|
||||||
|
"DELETE" => client.delete(&request.url),
|
||||||
|
"PATCH" => client.patch(&request.url),
|
||||||
|
"HEAD" => client.head(&request.url),
|
||||||
|
"OPTIONS" => client.request(reqwest::Method::OPTIONS, &request.url),
|
||||||
|
_ => {
|
||||||
|
return Err(ExtensionError::WebError {
|
||||||
|
reason: format!("Unsupported HTTP method: {}", method_str),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
if let Some(headers) = request.headers {
|
||||||
|
for (key, value) in headers {
|
||||||
|
req_builder = req_builder.header(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add body if present (decode from base64)
|
||||||
|
if let Some(body_base64) = request.body {
|
||||||
|
let body_bytes = STANDARD.decode(&body_base64).map_err(|e| {
|
||||||
|
ExtensionError::WebError {
|
||||||
|
reason: format!("Failed to decode request body from base64: {}", e),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
req_builder = req_builder.body(body_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute request
|
||||||
|
let response = req_builder.send().await.map_err(|e| {
|
||||||
|
if e.is_timeout() {
|
||||||
|
ExtensionError::WebError {
|
||||||
|
reason: format!("Request timeout after {}ms", timeout_ms),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ExtensionError::WebError {
|
||||||
|
reason: format!("Request failed: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Extract response data
|
||||||
|
let status = response.status().as_u16();
|
||||||
|
let status_text = response.status().canonical_reason().unwrap_or("").to_string();
|
||||||
|
let final_url = response.url().to_string();
|
||||||
|
|
||||||
|
// Extract headers
|
||||||
|
let mut response_headers = HashMap::new();
|
||||||
|
for (key, value) in response.headers() {
|
||||||
|
if let Ok(value_str) = value.to_str() {
|
||||||
|
response_headers.insert(key.to_string(), value_str.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read body and encode to base64
|
||||||
|
let body_bytes = response.bytes().await.map_err(|e| ExtensionError::WebError {
|
||||||
|
reason: format!("Failed to read response body: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let body_base64 = STANDARD.encode(&body_bytes);
|
||||||
|
|
||||||
|
Ok(WebFetchResponse {
|
||||||
|
status,
|
||||||
|
status_text,
|
||||||
|
headers: response_headers,
|
||||||
|
body: body_base64,
|
||||||
|
url: final_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -78,6 +78,8 @@ pub fn run() {
|
|||||||
database::vault_exists,
|
database::vault_exists,
|
||||||
extension::database::extension_sql_execute,
|
extension::database::extension_sql_execute,
|
||||||
extension::database::extension_sql_select,
|
extension::database::extension_sql_select,
|
||||||
|
extension::web::extension_web_fetch,
|
||||||
|
extension::web::extension_web_open,
|
||||||
extension::get_all_dev_extensions,
|
extension::get_all_dev_extensions,
|
||||||
extension::get_all_extensions,
|
extension::get_all_extensions,
|
||||||
extension::get_extension_info,
|
extension::get_extension_info,
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
handleDatabaseMethodAsync,
|
handleDatabaseMethodAsync,
|
||||||
handleFilesystemMethodAsync,
|
handleFilesystemMethodAsync,
|
||||||
handleHttpMethodAsync,
|
handleWebMethodAsync,
|
||||||
handlePermissionsMethodAsync,
|
handlePermissionsMethodAsync,
|
||||||
handleContextMethodAsync,
|
handleContextMethodAsync,
|
||||||
handleStorageMethodAsync,
|
handleStorageMethodAsync,
|
||||||
@ -165,8 +165,8 @@ const registerGlobalMessageHandler = () => {
|
|||||||
result = await handleDatabaseMethodAsync(request, instance.extension)
|
result = await handleDatabaseMethodAsync(request, instance.extension)
|
||||||
} else if (request.method.startsWith('haextension.fs.')) {
|
} else if (request.method.startsWith('haextension.fs.')) {
|
||||||
result = await handleFilesystemMethodAsync(request, instance.extension)
|
result = await handleFilesystemMethodAsync(request, instance.extension)
|
||||||
} else if (request.method.startsWith('haextension.http.')) {
|
} else if (request.method.startsWith('haextension.web.')) {
|
||||||
result = await handleHttpMethodAsync(request, instance.extension)
|
result = await handleWebMethodAsync(request, instance.extension)
|
||||||
} else if (request.method.startsWith('haextension.permissions.')) {
|
} else if (request.method.startsWith('haextension.permissions.')) {
|
||||||
result = await handlePermissionsMethodAsync(request, instance.extension)
|
result = await handlePermissionsMethodAsync(request, instance.extension)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
|
||||||
import type { ExtensionRequest } from './types'
|
|
||||||
|
|
||||||
export async function handleHttpMethodAsync(
|
|
||||||
request: ExtensionRequest,
|
|
||||||
extension: IHaexHubExtension,
|
|
||||||
) {
|
|
||||||
if (!extension || !request) {
|
|
||||||
throw new Error('Extension not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implementiere HTTP Commands im Backend
|
|
||||||
throw new Error('HTTP methods not yet implemented')
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// Export all handler functions
|
// Export all handler functions
|
||||||
export { handleDatabaseMethodAsync } from './database'
|
export { handleDatabaseMethodAsync } from './database'
|
||||||
export { handleFilesystemMethodAsync } from './filesystem'
|
export { handleFilesystemMethodAsync } from './filesystem'
|
||||||
export { handleHttpMethodAsync } from './http'
|
export { handleWebMethodAsync } from './web'
|
||||||
export { handlePermissionsMethodAsync } from './permissions'
|
export { handlePermissionsMethodAsync } from './permissions'
|
||||||
export { handleContextMethodAsync, setContextGetters } from './context'
|
export { handleContextMethodAsync, setContextGetters } from './context'
|
||||||
export { handleStorageMethodAsync } from './storage'
|
export { handleStorageMethodAsync } from './storage'
|
||||||
|
|||||||
96
src/composables/handlers/web.ts
Normal file
96
src/composables/handlers/web.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
|
import type { ExtensionRequest } from './types'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
export async function handleWebMethodAsync(
|
||||||
|
request: ExtensionRequest,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
if (!extension || !request) {
|
||||||
|
throw new Error('Extension not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { method, params } = request
|
||||||
|
|
||||||
|
if (method === 'haextension.web.fetch') {
|
||||||
|
return await handleWebFetchAsync(params, extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'haextension.web.open') {
|
||||||
|
return await handleWebOpenAsync(params, extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown web method: ${method}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWebFetchAsync(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
const url = params.url as string
|
||||||
|
const method = (params.method as string) || undefined
|
||||||
|
const headers = (params.headers as Record<string, string>) || undefined
|
||||||
|
const body = params.body as string | undefined
|
||||||
|
const timeout = (params.timeout as number) || undefined
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new Error('URL is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call Rust backend through Tauri IPC to avoid CORS restrictions
|
||||||
|
const response = await invoke<{
|
||||||
|
status: number
|
||||||
|
status_text: string
|
||||||
|
headers: Record<string, string>
|
||||||
|
body: string
|
||||||
|
url: string
|
||||||
|
}>('extension_web_fetch', {
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
timeout,
|
||||||
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.status_text,
|
||||||
|
headers: response.headers,
|
||||||
|
body: response.body,
|
||||||
|
url: response.url,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new Error(`Web request failed: ${error.message}`)
|
||||||
|
}
|
||||||
|
throw new Error('Web request failed with unknown error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWebOpenAsync(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
const url = params.url as string
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new Error('URL is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call Rust backend to open URL in default browser
|
||||||
|
await invoke<void>('extension_web_open', {
|
||||||
|
url,
|
||||||
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new Error(`Failed to open URL: ${error.message}`)
|
||||||
|
}
|
||||||
|
throw new Error('Failed to open URL with unknown error')
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user