use std::sync::Arc; use axum::{ extract::{Path, State}, routing::{get, post}, Json, Router, }; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::db; use crate::middleware::auth::AuthUser; use crate::models::error::{ApiError, ApiResult}; use crate::models::modules::{ModuleWithOwnership, PurchasedModule}; use crate::services::module_installer::ModuleInstaller; use crate::services::nats_bridge::NatsBridge; use crate::AppState; pub fn router() -> Router> { Router::new() .route("/catalog", get(get_catalog)) .route("/my-modules", get(get_my_modules)) .route("/purchase", post(purchase_module)) .route("/install", post(install_module)) .route("/:module_id/installation-status", get(get_installation_status)) } /// GET /api/modules/catalog /// Returns all modules with is_purchased flag for the authenticated user's license. async fn get_catalog( State(state): State>, auth: AuthUser, ) -> ApiResult> { let license_id = auth.license_id.ok_or_else(|| { ApiError::BadRequest("No license associated with this user".to_string()) })?; let catalog = db::modules::get_catalog_with_ownership(&state.db, license_id).await?; Ok(Json(CatalogResponse { modules: catalog })) } /// GET /api/modules/my-modules /// Returns purchased modules with installation status. async fn get_my_modules( State(state): State>, auth: AuthUser, ) -> ApiResult> { let license_id = auth.license_id.ok_or_else(|| { ApiError::BadRequest("No license associated with this user".to_string()) })?; let modules = db::modules::get_purchased_modules(&state.db, license_id).await?; Ok(Json(MyModulesResponse { modules })) } /// POST /api/modules/purchase /// Initiate module purchase (stub for Phase 4 — payment integration is XO's direct touch). /// For MVP, immediately records the purchase without payment gateway. async fn purchase_module( State(state): State>, auth: AuthUser, Json(req): Json, ) -> ApiResult> { let license_id = auth.license_id.ok_or_else(|| { ApiError::BadRequest("No license associated with this user".to_string()) })?; // Verify module exists let module = db::modules::get_module_by_id(&state.db, req.module_id) .await? .ok_or_else(|| ApiError::NotFound("Module not found".to_string()))?; // Check if already purchased let already_purchased = db::modules::is_module_purchased(&state.db, license_id, req.module_id).await?; if already_purchased { return Err(ApiError::Conflict("Module already purchased".to_string())); } // Record purchase (no payment gateway for MVP — stub transaction) let purchase_id = db::modules::record_module_purchase( &state.db, license_id, req.module_id, Some("STUB_TRANSACTION"), // TODO: Replace with real PayPal transaction ID Some(module.price_usd), ).await?; tracing::info!( "Module purchase recorded: license={}, module={}, purchase_id={}", license_id, req.module_id, purchase_id ); Ok(Json(PurchaseResponse { success: true, purchase_id, message: "Module purchased successfully (payment stub)".to_string(), })) } /// POST /api/modules/install /// Trigger module installation (calls panel adapter or companion agent). async fn install_module( State(state): State>, auth: AuthUser, Json(req): Json, ) -> ApiResult> { let license_id = auth.license_id.ok_or_else(|| { ApiError::BadRequest("No license associated with this user".to_string()) })?; // Verify ownership let is_purchased = db::modules::is_module_purchased(&state.db, license_id, req.module_id).await?; if !is_purchased { return Err(ApiError::Forbidden); } // Verify module exists and get plugin file URL let module = db::modules::get_module_by_id(&state.db, req.module_id) .await? .ok_or_else(|| ApiError::NotFound("Module not found".to_string()))?; // Ensure NATS is available let nats_client = state .nats .as_ref() .ok_or_else(|| ApiError::Internal("NATS not available".to_string()))?; let nats_bridge = Arc::new(NatsBridge::new(nats_client.clone())); let installer = ModuleInstaller::new( state.db.clone(), nats_bridge, state.config.encryption_key.clone(), ); // Set status to 'installing' immediately db::modules::update_installation_status( &state.db, license_id, req.module_id, "installing", None, ).await?; // Spawn background task for installation let module_name = module.name.clone(); tokio::spawn(async move { if let Err(e) = installer.install_module(license_id, req.module_id).await { tracing::error!( "Module installation failed for license {} module {}: {}", license_id, req.module_id, e ); } }); tracing::info!( "Module installation triggered: license={}, module={}", license_id, req.module_id ); Ok(Json(InstallResponse { success: true, message: format!("Installation of '{}' started. Check status for progress.", module_name), })) } /// GET /api/modules/:module_id/installation-status /// Get installation status for a specific module. async fn get_installation_status( State(state): State>, auth: AuthUser, Path(module_id): Path, ) -> ApiResult> { let license_id = auth.license_id.ok_or_else(|| { ApiError::BadRequest("No license associated with this user".to_string()) })?; let installation = db::modules::get_module_installation_status(&state.db, license_id, module_id).await?; Ok(Json(InstallationStatusResponse { module_id, status: installation.as_ref().map(|i| i.status.clone()), installed_at: installation.as_ref().and_then(|i| i.installed_at), error_message: installation.and_then(|i| i.error_message), })) } // ===== Request/Response Types ===== #[derive(Debug, Deserialize)] struct PurchaseRequest { module_id: Uuid, } #[derive(Debug, Serialize)] struct PurchaseResponse { success: bool, purchase_id: Uuid, message: String, } #[derive(Debug, Deserialize)] struct InstallRequest { module_id: Uuid, } #[derive(Debug, Serialize)] struct InstallResponse { success: bool, message: String, } #[derive(Debug, Serialize)] struct CatalogResponse { modules: Vec, } #[derive(Debug, Serialize)] struct MyModulesResponse { modules: Vec, } #[derive(Debug, Serialize)] struct InstallationStatusResponse { module_id: Uuid, status: Option, installed_at: Option>, error_message: Option, }