diff --git a/crates/pinakes-server/src/app.rs b/crates/pinakes-server/src/app.rs index fddc235..f32431c 100644 --- a/crates/pinakes-server/src/app.rs +++ b/crates/pinakes-server/src/app.rs @@ -480,7 +480,10 @@ pub fn create_router_with_tls( .route("/database/backup", post(routes::backup::create_backup)) // Plugin management .route("/plugins", get(routes::plugins::list_plugins)) + .route("/plugins/events", post(routes::plugins::emit_plugin_event)) .route("/plugins/ui-pages", get(routes::plugins::list_plugin_ui_pages)) + .route("/plugins/ui-widgets", get(routes::plugins::list_plugin_ui_widgets)) + .route("/plugins/ui-theme-extensions", get(routes::plugins::list_plugin_ui_theme_extensions)) .route("/plugins/{id}", get(routes::plugins::get_plugin)) .route("/plugins/install", post(routes::plugins::install_plugin)) .route("/plugins/{id}", delete(routes::plugins::uninstall_plugin)) diff --git a/crates/pinakes-server/src/dto/plugins.rs b/crates/pinakes-server/src/dto/plugins.rs index 4ec883d..a80a1f2 100644 --- a/crates/pinakes-server/src/dto/plugins.rs +++ b/crates/pinakes-server/src/dto/plugins.rs @@ -1,4 +1,4 @@ -use pinakes_plugin_api::UiPage; +use pinakes_plugin_api::{UiPage, UiWidget}; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize)] @@ -26,9 +26,29 @@ pub struct TogglePluginRequest { #[derive(Debug, Serialize)] pub struct PluginUiPageEntry { /// Plugin ID that provides this page - pub plugin_id: String, + pub plugin_id: String, /// Full page definition - pub page: UiPage, + pub page: UiPage, + /// Endpoint paths this plugin is allowed to fetch (empty means no + /// restriction) + pub allowed_endpoints: Vec, +} + +/// A single plugin UI widget entry in the list response +#[derive(Debug, Serialize)] +pub struct PluginUiWidgetEntry { + /// Plugin ID that provides this widget + pub plugin_id: String, + /// Full widget definition + pub widget: UiWidget, +} + +/// Request body for emitting a plugin event +#[derive(Debug, Deserialize)] +pub struct PluginEventRequest { + pub event: String, + #[serde(default)] + pub payload: serde_json::Value, } impl PluginResponse { diff --git a/crates/pinakes-server/src/routes/plugins.rs b/crates/pinakes-server/src/routes/plugins.rs index 429fb87..5653d23 100644 --- a/crates/pinakes-server/src/routes/plugins.rs +++ b/crates/pinakes-server/src/routes/plugins.rs @@ -1,28 +1,39 @@ +use std::{collections::HashMap, sync::Arc}; + use axum::{ Json, extract::{Path, State}, }; +use pinakes_core::plugin::PluginManager; use crate::{ dto::{ InstallPluginRequest, + PluginEventRequest, PluginResponse, PluginUiPageEntry, + PluginUiWidgetEntry, TogglePluginRequest, }, error::ApiError, state::AppState, }; +fn require_plugin_manager( + state: &AppState, +) -> Result, ApiError> { + state.plugin_manager.clone().ok_or_else(|| { + ApiError(pinakes_core::error::PinakesError::InvalidOperation( + "Plugin system is not enabled".to_string(), + )) + }) +} + /// List all installed plugins pub async fn list_plugins( State(state): State, ) -> Result>, ApiError> { - let plugin_manager = state.plugin_manager.as_ref().ok_or_else(|| { - ApiError(pinakes_core::error::PinakesError::InvalidOperation( - "Plugin system is not enabled".to_string(), - )) - })?; + let plugin_manager = require_plugin_manager(&state)?; let plugins = plugin_manager.list_plugins().await; let mut responses = Vec::with_capacity(plugins.len()); @@ -38,11 +49,7 @@ pub async fn get_plugin( State(state): State, Path(id): Path, ) -> Result, ApiError> { - let plugin_manager = state.plugin_manager.as_ref().ok_or_else(|| { - ApiError(pinakes_core::error::PinakesError::InvalidOperation( - "Plugin system is not enabled".to_string(), - )) - })?; + let plugin_manager = require_plugin_manager(&state)?; let plugin = plugin_manager.get_plugin(&id).await.ok_or_else(|| { ApiError(pinakes_core::error::PinakesError::NotFound(format!( @@ -59,11 +66,7 @@ pub async fn install_plugin( State(state): State, Json(req): Json, ) -> Result, ApiError> { - let plugin_manager = state.plugin_manager.as_ref().ok_or_else(|| { - ApiError(pinakes_core::error::PinakesError::InvalidOperation( - "Plugin system is not enabled".to_string(), - )) - })?; + let plugin_manager = require_plugin_manager(&state)?; let plugin_id = plugin_manager @@ -91,11 +94,7 @@ pub async fn uninstall_plugin( State(state): State, Path(id): Path, ) -> Result, ApiError> { - let plugin_manager = state.plugin_manager.as_ref().ok_or_else(|| { - ApiError(pinakes_core::error::PinakesError::InvalidOperation( - "Plugin system is not enabled".to_string(), - )) - })?; + let plugin_manager = require_plugin_manager(&state)?; plugin_manager.uninstall_plugin(&id).await.map_err(|e| { ApiError(pinakes_core::error::PinakesError::InvalidOperation( @@ -112,11 +111,7 @@ pub async fn toggle_plugin( Path(id): Path, Json(req): Json, ) -> Result, ApiError> { - let plugin_manager = state.plugin_manager.as_ref().ok_or_else(|| { - ApiError(pinakes_core::error::PinakesError::InvalidOperation( - "Plugin system is not enabled".to_string(), - )) - })?; + let plugin_manager = require_plugin_manager(&state)?; if req.enabled { plugin_manager.enable_plugin(&id).await.map_err(|e| { @@ -153,30 +148,61 @@ pub async fn toggle_plugin( pub async fn list_plugin_ui_pages( State(state): State, ) -> Result>, ApiError> { - let plugin_manager = state.plugin_manager.as_ref().ok_or_else(|| { - ApiError(pinakes_core::error::PinakesError::InvalidOperation( - "Plugin system is not enabled".to_string(), - )) - })?; + let plugin_manager = require_plugin_manager(&state)?; - let pages = plugin_manager.list_ui_pages().await; + let pages = plugin_manager.list_ui_pages_with_endpoints().await; let entries = pages .into_iter() - .map(|(plugin_id, page)| PluginUiPageEntry { plugin_id, page }) + .map(|(plugin_id, page, allowed_endpoints)| PluginUiPageEntry { + plugin_id, + page, + allowed_endpoints, + }) .collect(); Ok(Json(entries)) } +/// List all UI widgets provided by loaded plugins +pub async fn list_plugin_ui_widgets( + State(state): State, +) -> Result>, ApiError> { + let plugin_manager = require_plugin_manager(&state)?; + + let widgets = plugin_manager.list_ui_widgets().await; + let entries = widgets + .into_iter() + .map(|(plugin_id, widget)| PluginUiWidgetEntry { plugin_id, widget }) + .collect(); + Ok(Json(entries)) +} + +/// Receive a plugin event emitted from the UI and dispatch it to interested +/// server-side event-handler plugins via the pipeline. +pub async fn emit_plugin_event( + State(state): State, + Json(req): Json, +) -> Result, ApiError> { + tracing::info!(event = %req.event, "plugin UI event received"); + state.emit_plugin_event(&req.event, &req.payload); + Ok(Json( + serde_json::json!({ "received": true, "event": req.event }), + )) +} + +/// List merged CSS custom property overrides from all enabled plugins +pub async fn list_plugin_ui_theme_extensions( + State(state): State, +) -> Result>, ApiError> { + let plugin_manager = require_plugin_manager(&state)?; + Ok(Json(plugin_manager.list_ui_theme_extensions().await)) +} + /// Reload a plugin (for development) pub async fn reload_plugin( State(state): State, Path(id): Path, ) -> Result, ApiError> { - let plugin_manager = state.plugin_manager.as_ref().ok_or_else(|| { - ApiError(pinakes_core::error::PinakesError::InvalidOperation( - "Plugin system is not enabled".to_string(), - )) - })?; + let plugin_manager = require_plugin_manager(&state)?; plugin_manager.reload_plugin(&id).await.map_err(|e| { ApiError(pinakes_core::error::PinakesError::InvalidOperation(