mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 14:10:52 +01:00
extend extensions implementation
This commit is contained in:
@ -28,6 +28,72 @@ pub struct ExtensionManifest {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionInfoResponse {
|
||||
pub key_hash: String,
|
||||
pub name: String,
|
||||
pub full_id: String,
|
||||
pub version: String,
|
||||
pub display_name: Option<String>,
|
||||
pub namespace: Option<String>,
|
||||
pub allowed_origin: String,
|
||||
}
|
||||
|
||||
impl ExtensionInfoResponse {
|
||||
pub fn from_extension(extension: &Extension) -> Self {
|
||||
// Bestimme die allowed_origin basierend auf Tauri-Konfiguration
|
||||
let allowed_origin = get_tauri_origin();
|
||||
|
||||
Self {
|
||||
key_hash: calculate_key_hash(&extension.manifest.id),
|
||||
name: extension.manifest.name.clone(),
|
||||
full_id: format!(
|
||||
"{}/{}@{}",
|
||||
calculate_key_hash(&extension.manifest.id),
|
||||
extension.manifest.name,
|
||||
extension.manifest.version
|
||||
),
|
||||
version: extension.manifest.version.clone(),
|
||||
display_name: Some(extension.manifest.name.clone()),
|
||||
namespace: extension.manifest.author.clone(),
|
||||
allowed_origin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tauri_origin() -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
"https://tauri.localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// Dummy-Funktion für Key Hash (du implementierst das richtig mit SHA-256)
|
||||
fn calculate_key_hash(id: &str) -> String {
|
||||
// TODO: Implementiere SHA-256 Hash vom Public Key
|
||||
// Für jetzt nur Placeholder
|
||||
format!("{:0<20}", id.chars().take(20).collect::<String>())
|
||||
}
|
||||
/// Extension source type (production vs development)
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExtensionSource {
|
||||
|
||||
@ -16,7 +16,6 @@ pub struct DbExtensionPermission {
|
||||
pub extension_id: String,
|
||||
pub resource: String,
|
||||
pub operation: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Prüft Leseberechtigungen für eine Extension
|
||||
@ -168,7 +167,7 @@ async fn check_table_permissions(
|
||||
for table_name in table_names {
|
||||
let has_permission = permissions
|
||||
.iter()
|
||||
.any(|perm| perm.path.contains(table_name));
|
||||
.any(|perm| perm.resource.contains(table_name));
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
@ -207,7 +206,6 @@ pub async fn get_extension_permissions(
|
||||
extension_id: row.get(1)?,
|
||||
resource: row.get(2)?,
|
||||
operation: row.get(3)?,
|
||||
path: row.get(4)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| DatabaseError::QueryError {
|
||||
|
||||
@ -1,5 +1,19 @@
|
||||
use crate::extension::core::{ExtensionInfoResponse, ExtensionManager};
|
||||
use tauri::State;
|
||||
pub mod core;
|
||||
pub mod database;
|
||||
pub mod error;
|
||||
pub mod filesystem;
|
||||
pub mod permission_manager;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_extension_info(
|
||||
extension_id: String,
|
||||
extension_manager: State<ExtensionManager>,
|
||||
) -> Result<ExtensionInfoResponse, String> {
|
||||
let extension = extension_manager
|
||||
.get_extension(&extension_id)
|
||||
.ok_or_else(|| format!("Extension nicht gefunden: {}", extension_id))?;
|
||||
|
||||
Ok(ExtensionInfoResponse::from_extension(&extension))
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ impl PermissionManager {
|
||||
|
||||
let has_permission = permissions
|
||||
.iter()
|
||||
.any(|perm| perm.path.contains(table_name));
|
||||
.any(|perm| perm.resource.contains(table_name));
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
|
||||
52
src/composables/extensionContextBroadcast.ts
Normal file
52
src/composables/extensionContextBroadcast.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Broadcasts context changes to all active extensions
|
||||
*/
|
||||
export const useExtensionContextBroadcast = () => {
|
||||
const extensionIframes = ref<HTMLIFrameElement[]>([])
|
||||
|
||||
const registerExtensionIframe = (iframe: HTMLIFrameElement) => {
|
||||
extensionIframes.value.push(iframe)
|
||||
}
|
||||
|
||||
const unregisterExtensionIframe = (iframe: HTMLIFrameElement) => {
|
||||
extensionIframes.value = extensionIframes.value.filter((f) => f !== iframe)
|
||||
}
|
||||
|
||||
const broadcastContextChange = (context: {
|
||||
theme: string
|
||||
locale: string
|
||||
platform: string
|
||||
}) => {
|
||||
const message = {
|
||||
type: 'context.changed',
|
||||
data: { context },
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
extensionIframes.value.forEach((iframe) => {
|
||||
iframe.contentWindow?.postMessage(message, '*')
|
||||
})
|
||||
}
|
||||
|
||||
const broadcastSearchRequest = (query: string, requestId: string) => {
|
||||
const message = {
|
||||
type: 'search.request',
|
||||
data: {
|
||||
query: { query, limit: 10 },
|
||||
requestId,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
extensionIframes.value.forEach((iframe) => {
|
||||
iframe.contentWindow?.postMessage(message, '*')
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
registerExtensionIframe,
|
||||
unregisterExtensionIframe,
|
||||
broadcastContextChange,
|
||||
broadcastSearchRequest,
|
||||
}
|
||||
}
|
||||
358
src/composables/extensionMessageHandler.ts
Normal file
358
src/composables/extensionMessageHandler.ts
Normal file
@ -0,0 +1,358 @@
|
||||
import type { IHaexHubExtensionLink } from '~/types/haexhub'
|
||||
|
||||
interface ExtensionRequest {
|
||||
id: string
|
||||
method: string
|
||||
params: Record<string, unknown>
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
interface ExtensionResponse {
|
||||
id: string
|
||||
result?: unknown
|
||||
error?: {
|
||||
code: string
|
||||
message: string
|
||||
details?: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export const useExtensionMessageHandler = (
|
||||
iframeRef: Ref<HTMLIFrameElement | undefined | null>,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
) => {
|
||||
const handleMessage = async (event: MessageEvent) => {
|
||||
// Security: Only accept messages from our iframe
|
||||
if (!iframeRef.value || event.source !== iframeRef.value.contentWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
const request = event.data as ExtensionRequest
|
||||
|
||||
// Validate request structure
|
||||
if (!request.id || !request.method) {
|
||||
console.error('Invalid extension request:', request)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[HaexHub] Extension request:', request.method, request.params)
|
||||
|
||||
try {
|
||||
let result: unknown
|
||||
|
||||
// Route request to appropriate handler
|
||||
if (request.method.startsWith('extension.')) {
|
||||
result = await handleExtensionMethod(request, extension)
|
||||
} else if (request.method.startsWith('db.')) {
|
||||
result = await handleDatabaseMethod(request, extension)
|
||||
} else if (request.method.startsWith('permissions.')) {
|
||||
result = await handlePermissionsMethod(request, extension)
|
||||
} else if (request.method.startsWith('context.')) {
|
||||
result = await handleContextMethod(request)
|
||||
} else if (request.method.startsWith('search.')) {
|
||||
result = await handleSearchMethod(request, extension)
|
||||
} else {
|
||||
throw new Error(`Unknown method: ${request.method}`)
|
||||
}
|
||||
|
||||
// Send success response
|
||||
sendResponse(iframeRef.value, {
|
||||
id: request.id,
|
||||
result,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[HaexHub] Extension request error:', error)
|
||||
|
||||
// Send error response
|
||||
sendResponse(iframeRef.value, {
|
||||
id: request.id,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
details: error,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const sendResponse = (
|
||||
iframe: HTMLIFrameElement,
|
||||
response: ExtensionResponse,
|
||||
) => {
|
||||
iframe.contentWindow?.postMessage(response, '*')
|
||||
}
|
||||
|
||||
// Register/unregister message listener
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', handleMessage)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
})
|
||||
|
||||
return {
|
||||
handleMessage,
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Extension Methods
|
||||
// ==========================================
|
||||
|
||||
async function handleExtensionMethod(
|
||||
request: ExtensionRequest,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
) {
|
||||
switch (request.method) {
|
||||
case 'extension.getInfo':
|
||||
return {
|
||||
keyHash: extension.value?.id || '', // TODO: Real key hash
|
||||
name: extension.value?.name || '',
|
||||
fullId: `${extension.value?.id}/${extension.value?.name}@${extension.value?.version}`,
|
||||
version: extension.value?.version || '',
|
||||
displayName: extension.value?.name,
|
||||
namespace: extension.value?.author,
|
||||
allowedOrigin: window.location.origin, // "tauri://localhost"
|
||||
}
|
||||
|
||||
case 'extensions.getDependencies':
|
||||
// TODO: Implement dependencies from manifest
|
||||
return []
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown extension method: ${request.method}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Database Methods
|
||||
// ==========================================
|
||||
|
||||
async function handleDatabaseMethod(
|
||||
request: ExtensionRequest,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
) {
|
||||
const { currentVault } = useVaultStore()
|
||||
if (!currentVault) {
|
||||
throw new Error('No vault available')
|
||||
}
|
||||
|
||||
if (!extension.value) {
|
||||
throw new Error('Extension not found')
|
||||
}
|
||||
|
||||
const params = request.params as { query?: string; params?: unknown[] }
|
||||
|
||||
switch (request.method) {
|
||||
case 'db.query': {
|
||||
// Validate permission
|
||||
await validateDatabaseAccess(extension.value, params.query || '', 'read')
|
||||
|
||||
// Execute query
|
||||
const result = await currentVault.drizzle.execute(params.query || '')
|
||||
|
||||
return {
|
||||
rows: result.rows || [],
|
||||
rowsAffected: 0,
|
||||
lastInsertId: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
case 'db.execute': {
|
||||
// Validate permission
|
||||
await validateDatabaseAccess(extension.value, params.query || '', 'write')
|
||||
|
||||
// Execute query
|
||||
const result = await currentVault.drizzle.execute(params.query || '')
|
||||
|
||||
return {
|
||||
rows: [],
|
||||
rowsAffected: result.rowsAffected || 0,
|
||||
lastInsertId: result.lastInsertId,
|
||||
}
|
||||
}
|
||||
|
||||
case 'db.transaction': {
|
||||
const statements =
|
||||
(request.params as { statements?: string[] }).statements || []
|
||||
|
||||
// Validate all statements
|
||||
for (const stmt of statements) {
|
||||
await validateDatabaseAccess(extension.value, stmt, 'write')
|
||||
}
|
||||
|
||||
// Execute transaction
|
||||
await currentVault.drizzle.transaction(async (tx) => {
|
||||
for (const stmt of statements) {
|
||||
await tx.execute(stmt)
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown database method: ${request.method}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Permission Validation
|
||||
// ==========================================
|
||||
|
||||
async function validateDatabaseAccess(
|
||||
extension: IHaexHubExtensionLink,
|
||||
query: string,
|
||||
operation: 'read' | 'write',
|
||||
): Promise<void> {
|
||||
// Extract table name from query
|
||||
const tableMatch = query.match(/(?:FROM|INTO|UPDATE|TABLE)\s+(\w+)/i)
|
||||
if (!tableMatch) {
|
||||
throw new Error('Could not extract table name from query')
|
||||
}
|
||||
|
||||
const tableName = tableMatch[1]
|
||||
|
||||
// Check if it's the extension's own table
|
||||
const extensionPrefix = `${extension.id}_${extension.name?.replace(/-/g, '_')}_`
|
||||
const isOwnTable = tableName.startsWith(extensionPrefix)
|
||||
|
||||
if (isOwnTable) {
|
||||
// Own tables: always allowed
|
||||
return
|
||||
}
|
||||
|
||||
// External table: Check permissions
|
||||
const hasPermission = await checkDatabasePermission(
|
||||
extension.id,
|
||||
tableName,
|
||||
operation,
|
||||
)
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new Error(`Permission denied: ${operation} access to ${tableName}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDatabasePermission(
|
||||
extensionId: string,
|
||||
tableName: string,
|
||||
operation: 'read' | 'write',
|
||||
): Promise<boolean> {
|
||||
// TODO: Query permissions from database
|
||||
// SELECT * FROM db_extension_permissions
|
||||
// WHERE extension_id = ? AND resource = ? AND operation = ?
|
||||
|
||||
console.warn('TODO: Implement permission check', {
|
||||
extensionId,
|
||||
tableName,
|
||||
operation,
|
||||
})
|
||||
|
||||
// For now: deny by default
|
||||
return false
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Permission Methods
|
||||
// ==========================================
|
||||
|
||||
async function handlePermissionsMethod(
|
||||
request: ExtensionRequest,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
) {
|
||||
switch (request.method) {
|
||||
case 'permissions.database.request': {
|
||||
const params = request.params as {
|
||||
resource: string
|
||||
operation: 'read' | 'write'
|
||||
reason?: string
|
||||
}
|
||||
|
||||
// TODO: Show user dialog to grant/deny permission
|
||||
console.log('[HaexHub] Permission request:', params)
|
||||
|
||||
// For now: return ASK
|
||||
return {
|
||||
status: 'ask',
|
||||
permanent: false,
|
||||
}
|
||||
}
|
||||
|
||||
case 'permissions.database.check': {
|
||||
const params = request.params as {
|
||||
resource: string
|
||||
operation: 'read' | 'write'
|
||||
}
|
||||
|
||||
const hasPermission = await checkDatabasePermission(
|
||||
extension.value?.id || '',
|
||||
params.resource,
|
||||
params.operation,
|
||||
)
|
||||
|
||||
return {
|
||||
status: hasPermission ? 'granted' : 'denied',
|
||||
permanent: true,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown permission method: ${request.method}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Context Methods
|
||||
// ==========================================
|
||||
|
||||
async function handleContextMethod(request: ExtensionRequest) {
|
||||
const { theme } = useThemeStore()
|
||||
const { locale } = useI18n()
|
||||
|
||||
switch (request.method) {
|
||||
case 'context.get':
|
||||
return {
|
||||
theme: theme.value || 'system',
|
||||
locale: locale.value,
|
||||
platform: detectPlatform(),
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown context method: ${request.method}`)
|
||||
}
|
||||
}
|
||||
|
||||
function detectPlatform(): 'desktop' | 'mobile' | 'tablet' {
|
||||
const width = window.innerWidth
|
||||
if (width < 768) return 'mobile'
|
||||
if (width < 1024) return 'tablet'
|
||||
return 'desktop'
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Search Methods
|
||||
// ==========================================
|
||||
|
||||
async function handleSearchMethod(
|
||||
request: ExtensionRequest,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
) {
|
||||
switch (request.method) {
|
||||
case 'search.respond': {
|
||||
const params = request.params as {
|
||||
requestId: string
|
||||
results: unknown[]
|
||||
}
|
||||
|
||||
// TODO: Store search results for display
|
||||
console.log('[HaexHub] Search results from extension:', params)
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown search method: ${request.method}`)
|
||||
}
|
||||
}
|
||||
@ -1,29 +1,68 @@
|
||||
<template>
|
||||
<div class="w-full h-full overflow-scroll">
|
||||
<div>
|
||||
{{ iframeIndex }}
|
||||
<div
|
||||
v-if="!iFrameSrc"
|
||||
class="flex items-center justify-center h-full"
|
||||
>
|
||||
<p>{{ t('loading') }}</p>
|
||||
</div>
|
||||
<iframe
|
||||
v-if="iframeIndex"
|
||||
v-else
|
||||
ref="iFrameRef"
|
||||
class="w-full h-full"
|
||||
:src="iframeIndex"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
:src="iFrameSrc"
|
||||
sandbox="allow-scripts "
|
||||
allow="autoplay; speaker-selection; encrypted-media;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useExtensionMessageHandler } from '~/composables/extensionMessageHandler'
|
||||
|
||||
definePageMeta({
|
||||
name: 'haexExtension',
|
||||
})
|
||||
|
||||
const { extensionEntry: iframeSrc } = storeToRefs(useExtensionsStore())
|
||||
const { t } = useI18n()
|
||||
|
||||
const iframeIndex = computed(() =>
|
||||
const iFrameRef = useTemplateRef('iFrameRef')
|
||||
|
||||
const { extensionEntry: iframeSrc, currentExtension } =
|
||||
storeToRefs(useExtensionsStore())
|
||||
|
||||
const iFrameSrc = computed(() =>
|
||||
iframeSrc.value ? `${iframeSrc.value}/index.html` : '',
|
||||
)
|
||||
|
||||
useExtensionMessageHandler(iFrameRef, currentExtension)
|
||||
|
||||
const { currentTheme } = storeToRefs(useUiStore())
|
||||
const { locale } = useI18n()
|
||||
|
||||
watch([currentTheme, locale], () => {
|
||||
if (iFrameRef.value?.contentWindow) {
|
||||
iFrameRef.value.contentWindow.postMessage(
|
||||
{
|
||||
type: 'context.changed',
|
||||
data: {
|
||||
context: {
|
||||
theme: currentTheme.value || 'system',
|
||||
locale: locale.value,
|
||||
platform:
|
||||
window.innerWidth < 768
|
||||
? 'mobile'
|
||||
: window.innerWidth < 1024
|
||||
? 'tablet'
|
||||
: 'desktop',
|
||||
},
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
'*',
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// src/stores/vault/index.ts
|
||||
|
||||
import { drizzle } from 'drizzle-orm/sqlite-proxy'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import * as schema from '@/../src-tauri/database/schemas/vault'
|
||||
|
||||
Reference in New Issue
Block a user