This repository has been archived on 2025-12-01. You can view files and clone it, but cannot push or open issues or pull requests.
Files
projectmycelium_old/src/controllers/marketplace.rs

2020 lines
94 KiB
Rust

use actix_web::{web, Result, Responder};
use tera::Tera;
use crate::utils::render_template;
use crate::utils::response_builder::ResponseBuilder;
use crate::config::get_app_config;
use crate::services::{currency::CurrencyService, product::ProductService, node_marketplace::NodeMarketplaceService, slice_rental::SliceRentalService, slice_assignment::{SliceAssignmentService, SliceAssignmentRequest, DeploymentConfiguration, AssignmentMode, NetworkConfiguration, SecurityConfiguration, VMConfiguration}};
use crate::services::slice_assignment::DeploymentType as SliceDeploymentType;
use actix_session::Session;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::Utc;
/// Form data for slice rental requests
#[derive(Debug, Deserialize)]
pub struct SliceRentalForm {
pub farmer_email: String,
pub node_id: String,
pub combination_id: String,
pub quantity: u32,
pub rental_duration_hours: u32,
pub deployment_type: String, // "vm" or "kubernetes"
// VM-specific options
pub vm_name: Option<String>,
pub vm_os: Option<String>, // "ubuntu", "debian", "centos", "alpine"
pub vm_ssh_key: Option<String>,
// Kubernetes-specific options
pub k8s_masters: Option<u32>,
pub k8s_workers: Option<u32>,
pub cluster_name: Option<String>,
pub k8s_version: Option<String>, // "1.28", "1.29", "1.30"
pub k8s_network_plugin: Option<String>, // "flannel", "calico", "weave"
// Common deployment options
pub auto_scaling: Option<bool>,
pub backup_enabled: Option<bool>,
pub monitoring_enabled: Option<bool>,
}
/// Deployment configuration for slice rentals
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SliceDeploymentConfig {
pub deployment_id: String,
pub deployment_type: DeploymentType,
pub slice_specs: SliceSpecs,
pub network_config: NetworkConfig,
pub security_config: SecurityConfig,
pub created_at: chrono::DateTime<chrono::Utc>,
pub status: DeploymentStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DeploymentType {
VM {
name: String,
os: String,
ssh_key: Option<String>,
},
Kubernetes {
cluster_name: String,
masters: u32,
workers: u32,
version: String,
network_plugin: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SliceSpecs {
pub cpu_cores: u32,
pub memory_gb: u32,
pub storage_gb: u32,
pub quantity: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
pub public_ip: bool,
pub private_network: String,
pub ports: Vec<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
pub firewall_enabled: bool,
pub ssh_access: bool,
pub monitoring: bool,
pub backup: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DeploymentStatus {
Pending,
Deploying,
Running,
Stopped,
Failed,
Terminated,
}
/// Controller for handling marketplace-related routes
pub struct MarketplaceController;
impl MarketplaceController {
/// Renders the marketplace dashboard page
pub async fn dashboard(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let product_service = ProductService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let currency_service = CurrencyService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("marketplace")
.build();
ctx.insert("active_section", "dashboard");
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Get user's preferred currency
let display_currency = currency_service.get_user_preferred_currency(&session);
// Get marketplace data (gate mock usage)
// Get marketplace configuration
let mut marketplace_config = crate::models::marketplace::MarketplaceConfig::default();
marketplace_config.marketplace.default_display_currency = display_currency.clone();
let featured_products = product_service.get_featured_products();
let categories = product_service.get_categories();
let product_stats = product_service.get_product_statistics();
// Get popular applications (first 3 application products)
let app_criteria = crate::services::product::ProductSearchCriteria::new()
.with_category("application".to_string());
let app_search_result = product_service.search_products_advanced(&app_criteria, 0, 3);
// Get available services (first 3 service products)
let service_criteria = crate::services::product::ProductSearchCriteria::new()
.with_category("service".to_string());
let service_search_result = product_service.search_products_advanced(&service_criteria, 0, 3);
// De-duplicate: exclude any application already featured
let featured_ids: std::collections::HashSet<String> =
featured_products.iter().map(|p| p.id.clone()).collect();
let app_products_dedup: Vec<_> = app_search_result
.products
.into_iter()
.filter(|p| !featured_ids.contains(&p.id))
.collect();
// Convert featured product prices to user's currency
let featured_with_prices = match product_service.get_products_with_converted_prices(
&featured_products,
&display_currency,
) {
Ok(converted) => converted.into_iter()
.map(|(product, price)| {
let formatted_price = currency_service.format_price(
price.display_amount,
&price.display_currency,
).unwrap_or_else(|_| format!("{} {}", price.display_amount, price.display_currency));
serde_json::json!({
"product": product,
"price": price,
"formatted_price": formatted_price
})
})
.collect::<Vec<_>>(),
Err(_) => Vec::new(),
};
// Convert application prices to user's currency
let applications_with_prices = match product_service.get_products_with_converted_prices(
&app_products_dedup,
&display_currency,
) {
Ok(converted) => converted.into_iter()
.map(|(product, price)| {
let formatted_price = currency_service.format_price(
price.display_amount,
&price.display_currency,
).unwrap_or_else(|_| format!("{} {}", price.display_amount, price.display_currency));
serde_json::json!({
"product": product,
"price": price,
"formatted_price": formatted_price
})
})
.collect::<Vec<_>>(),
Err(_) => Vec::new(),
};
// Convert service prices to user's currency
let services_with_prices = match product_service.get_products_with_converted_prices(
&service_search_result.products,
&display_currency,
) {
Ok(converted) => converted.into_iter()
.map(|(product, price)| {
let formatted_price = currency_service.format_price(
price.display_amount,
&price.display_currency,
).unwrap_or_else(|_| format!("{} {}", price.display_amount, price.display_currency));
serde_json::json!({
"product": product,
"price": price,
"formatted_price": formatted_price
})
})
.collect::<Vec<_>>(),
Err(_) => Vec::new(),
};
// Add marketplace data to context
ctx.insert("marketplace_config", &marketplace_config);
ctx.insert("featured_products", &featured_with_prices);
ctx.insert("popular_applications", &applications_with_prices);
ctx.insert("available_services", &services_with_prices);
ctx.insert("categories", &categories);
ctx.insert("product_stats", &product_stats);
ctx.insert("currencies", &currency_service.get_currency_display_info());
ctx.insert("user_currency", &display_currency);
// Add user to context if available
if let Ok(Some(user_json)) = session.get::<String>("user") {
// Keep the raw JSON for backward compatibility
ctx.insert("user_json", &user_json);
// Parse the JSON into a User object
match serde_json::from_str::<crate::models::user::User>(&user_json) {
Ok(user) => {
ctx.insert("user", &user);
},
Err(e) => {
}
}
}
render_template(&tmpl, "marketplace/dashboard.html", &ctx)
}
/// Renders the compute resources (slices) marketplace page
pub async fn compute_resources(tmpl: web::Data<Tera>, session: Session, query: web::Query<std::collections::HashMap<String, String>>) -> Result<impl Responder> {
let currency_service = CurrencyService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let node_marketplace_service = NodeMarketplaceService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("marketplace")
.build();
ctx.insert("active_section", "compute_resources");
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Get user's preferred currency
let display_currency = currency_service.get_user_preferred_currency(&session);
// Get all slice combinations from the marketplace
let all_slice_products = node_marketplace_service.get_all_slice_combinations();
// Log first few products for debugging
for (i, product) in all_slice_products.iter().take(3).enumerate() {
}
// Apply slice-specific filters
let filtered_products = node_marketplace_service.apply_slice_filters(&all_slice_products, &query);
// Parse pagination parameters
let page = query.get("page")
.and_then(|p| p.parse::<usize>().ok())
.unwrap_or(0);
let page_size = 12; // Products per page
// Apply pagination
let total_count = filtered_products.len();
let total_pages = (total_count + page_size - 1) / page_size;
let start_index = page * page_size;
let end_index = std::cmp::min(start_index + page_size, total_count);
let paginated_products = if start_index < total_count {
filtered_products[start_index..end_index].to_vec()
} else {
Vec::new()
};
// Convert prices to user's currency and format for display
let products_with_prices = paginated_products.into_iter()
.map(|product| {
// Convert price to user's preferred currency using create_price
let converted_price = currency_service.create_price(
product.base_price,
&product.base_currency,
&display_currency,
).unwrap_or_else(|_| crate::models::currency::Price {
base_amount: product.base_price,
base_currency: product.base_currency.clone(),
display_currency: display_currency.clone(),
display_amount: product.base_price,
formatted_display: format!("{} {}", product.base_price, product.base_currency),
conversion_rate: rust_decimal::Decimal::from(1),
conversion_timestamp: chrono::Utc::now(),
});
let formatted_price = currency_service.format_price(
converted_price.display_amount,
&converted_price.display_currency,
).unwrap_or_else(|_| format!("{} {}", converted_price.display_amount, converted_price.display_currency));
serde_json::json!({
"product": product,
"price": converted_price,
"formatted_price": formatted_price
})
})
.collect::<Vec<_>>();
ctx.insert("compute_products", &products_with_prices);
ctx.insert("slice_products", &products_with_prices); // Keep both for compatibility
ctx.insert("currencies", &currency_service.get_currency_display_info());
ctx.insert("user_currency", &display_currency);
// Add pagination data
ctx.insert("pagination", &serde_json::json!({
"current_page": page,
"total_pages": total_pages,
"total_count": total_count,
"page_size": page_size,
"has_previous": page > 0,
"has_next": page < total_pages.saturating_sub(1),
"previous_page": page.saturating_sub(1),
"next_page": page + 1
}));
// Add slice marketplace statistics
let slice_stats = node_marketplace_service.get_slice_marketplace_statistics();
ctx.insert("slice_statistics", &slice_stats);
// Add available filter options
let available_locations: Vec<String> = all_slice_products.iter()
.filter_map(|p| p.attributes.get("location"))
.filter_map(|l| l.value.as_str())
.map(|l| l.to_string())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
ctx.insert("available_locations", &available_locations);
let available_certifications: Vec<String> = all_slice_products.iter()
.filter_map(|p| p.attributes.get("node_characteristics"))
.filter_map(|chars| chars.value.get("certification_type"))
.filter_map(|cert| cert.as_str())
.map(|cert| cert.to_string())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
ctx.insert("available_certifications", &available_certifications);
// Add current filters to context
ctx.insert("current_filters", &query.into_inner());
// Add user data to context
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
if let Ok(user) = serde_json::from_str::<crate::models::user::User>(&user_json) {
ctx.insert("user", &user);
}
}
render_template(&tmpl, "marketplace/compute_resources.html", &ctx)
}
/// Renders the Mycelium Nodes marketplace page with REAL farmer nodes from database
pub async fn mycelium_nodes(tmpl: web::Data<Tera>, session: Session, query: web::Query<std::collections::HashMap<String, String>>) -> Result<impl Responder> {
// Build services using established builder pattern
let currency_service = CurrencyService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let node_marketplace_service = NodeMarketplaceService::builder()
.currency_service(currency_service.clone())
.include_offline_nodes(false) // Only show online nodes in marketplace
.price_calculation_method("capacity_based")
.cache_enabled(true)
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
// Build context using ContextBuilder pattern
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("marketplace")
.active_section("mycelium_nodes")
.user_if_available(&session)
.build();
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Get user's preferred currency
let display_currency = currency_service.get_user_preferred_currency(&session);
// Parse pagination parameters
let page = query.get("page")
.and_then(|p| p.parse::<usize>().ok())
.unwrap_or(0);
let page_size = 12;
// Get all real farmer nodes as marketplace products
let all_node_products = node_marketplace_service.get_all_marketplace_nodes();
// Clone query for reuse
let query_params = query.into_inner();
// Apply filters using the service's filter method
let filtered_products = node_marketplace_service.apply_marketplace_filters(&all_node_products, &query_params);
// Apply pagination
let total_count = filtered_products.len();
let total_pages = (total_count + page_size - 1) / page_size;
let start_idx = page * page_size;
let end_idx = std::cmp::min(start_idx + page_size, total_count);
let paginated_products = if start_idx < filtered_products.len() {
filtered_products[start_idx..end_idx].to_vec()
} else {
Vec::new()
};
// Convert prices to user's currency
let products_with_prices = paginated_products.into_iter()
.map(|product| {
// Convert price to user's preferred currency using create_price
let converted_price = currency_service.create_price(
product.base_price,
&product.base_currency,
&display_currency,
).unwrap_or_else(|_| crate::models::currency::Price {
base_amount: product.base_price,
base_currency: product.base_currency.clone(),
display_currency: display_currency.clone(),
display_amount: product.base_price,
formatted_display: format!("{} {}", product.base_price, product.base_currency),
conversion_rate: rust_decimal::Decimal::from(1),
conversion_timestamp: chrono::Utc::now(),
});
let formatted_price = currency_service.format_price(
converted_price.display_amount,
&converted_price.display_currency,
).unwrap_or_else(|_| format!("{} {}", converted_price.display_amount, converted_price.display_currency));
serde_json::json!({
"product": product,
"price": converted_price,
"formatted_price": formatted_price
})
})
.collect::<Vec<_>>();
// Add data to context
ctx.insert("hardware_products", &products_with_prices);
ctx.insert("currencies", &currency_service.get_currency_display_info());
ctx.insert("user_currency", &display_currency);
ctx.insert("current_filters", &query_params);
// Add pagination data
ctx.insert("pagination", &serde_json::json!({
"current_page": page,
"total_pages": total_pages,
"total_count": total_count,
"page_size": page_size,
"has_previous": page > 0,
"has_next": page < total_pages.saturating_sub(1),
"previous_page": page.saturating_sub(1),
"next_page": page + 1
}));
// Add node marketplace statistics
ctx.insert("node_statistics", &node_marketplace_service.get_capacity_statistics());
ctx.insert("available_regions", &node_marketplace_service.get_available_regions());
render_template(&tmpl, "marketplace/mycelium_nodes.html", &ctx)
}
/// Renders the gateways marketplace page
pub async fn gateways(tmpl: web::Data<Tera>, session: Session, query: web::Query<std::collections::HashMap<String, String>>) -> Result<impl Responder> {
let product_service = ProductService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let currency_service = CurrencyService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("marketplace")
.build();
ctx.insert("active_section", "gateways");
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Get user's preferred currency
let display_currency = currency_service.get_user_preferred_currency(&session);
// Parse pagination and filter parameters
let page = query.get("page")
.and_then(|p| p.parse::<usize>().ok())
.unwrap_or(0);
let page_size = 12; // Products per page
// Build search criteria from query parameters
let mut criteria = crate::services::product::ProductSearchCriteria::new()
.with_category("gateway".to_string());
// Add filters from query parameters
if let Some(location) = query.get("location") {
if !location.is_empty() {
criteria = criteria.with_location(location.clone());
}
}
if let Some(bandwidth) = query.get("bandwidth_mbps") {
if let Ok(bw) = bandwidth.parse::<i64>() {
criteria = criteria.with_attribute("bandwidth_mbps".to_string(), serde_json::Value::Number(serde_json::Number::from(bw)));
}
}
let mut min_price_val = None;
let mut max_price_val = None;
if let Some(min_price) = query.get("min_price") {
if let Ok(price) = min_price.parse::<rust_decimal::Decimal>() {
min_price_val = Some(price);
}
}
if let Some(max_price) = query.get("max_price") {
if let Ok(price) = max_price.parse::<rust_decimal::Decimal>() {
max_price_val = Some(price);
}
}
if min_price_val.is_some() || max_price_val.is_some() {
criteria = criteria.with_price_range(min_price_val, max_price_val);
}
// Search products with pagination
let search_result = product_service.search_products_advanced(&criteria, page, page_size);
// Convert prices to user's currency
let products_with_prices = match product_service.get_products_with_converted_prices(
&search_result.products,
&display_currency,
) {
Ok(converted) => converted.into_iter()
.map(|(product, price)| {
let formatted_price = currency_service.format_price(
price.display_amount,
&price.display_currency,
).unwrap_or_else(|_| format!("{} {}", price.display_amount, price.display_currency));
serde_json::json!({
"product": product,
"price": price,
"formatted_price": formatted_price
})
})
.collect::<Vec<_>>(),
Err(_) => Vec::new(),
};
ctx.insert("gateway_products", &products_with_prices);
ctx.insert("currencies", &currency_service.get_currency_display_info());
ctx.insert("user_currency", &display_currency);
// Add pagination data
ctx.insert("pagination", &serde_json::json!({
"current_page": search_result.page,
"total_pages": search_result.total_pages,
"total_count": search_result.total_count,
"page_size": search_result.page_size,
"has_previous": search_result.page > 0,
"has_next": search_result.page < search_result.total_pages.saturating_sub(1),
"previous_page": search_result.page.saturating_sub(1),
"next_page": search_result.page + 1
}));
// Add current filters to context
ctx.insert("current_filters", &query.into_inner());
// Add user data to context
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
if let Ok(user) = serde_json::from_str::<crate::models::user::User>(&user_json) {
ctx.insert("user", &user);
}
}
render_template(&tmpl, "marketplace/gateways.html", &ctx)
}
/// Renders the applications marketplace page
pub async fn applications(tmpl: web::Data<Tera>, session: Session, query: web::Query<std::collections::HashMap<String, String>>) -> Result<impl Responder> {
let product_service = ProductService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let currency_service = CurrencyService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("marketplace")
.build();
ctx.insert("active_section", "applications");
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Get user's preferred currency
let display_currency = currency_service.get_user_preferred_currency(&session);
// Parse pagination and filter parameters
let page = query.get("page")
.and_then(|p| p.parse::<usize>().ok())
.unwrap_or(0);
let page_size = 12; // Products per page
// Build search criteria from query parameters
let mut criteria = crate::services::product::ProductSearchCriteria::new()
.with_category("application".to_string());
// Add filters from query parameters
if let Some(app_type) = query.get("app_type") {
if !app_type.is_empty() {
criteria = criteria.with_attribute("app_type".to_string(), serde_json::Value::String(app_type.clone()));
}
}
if let Some(deployment_type) = query.get("deployment_type") {
if !deployment_type.is_empty() {
criteria = criteria.with_attribute("deployment_type".to_string(), serde_json::Value::String(deployment_type.clone()));
}
}
let mut min_price_val = None;
let mut max_price_val = None;
if let Some(min_price) = query.get("min_price") {
if let Ok(price) = min_price.parse::<rust_decimal::Decimal>() {
min_price_val = Some(price);
}
}
if let Some(max_price) = query.get("max_price") {
if let Ok(price) = max_price.parse::<rust_decimal::Decimal>() {
max_price_val = Some(price);
}
}
if min_price_val.is_some() || max_price_val.is_some() {
criteria = criteria.with_price_range(min_price_val, max_price_val);
}
// Search products with pagination
let search_result = product_service.search_products_advanced(&criteria, page, page_size);
// Debug: Log all product IDs found
for product in &search_result.products {
}
// Get user-created apps from all users' persistent data (global marketplace)
let mut all_applications = search_result.products.clone();
// Load apps from all users' persistent data for marketplace display
let user_apps = crate::services::user_persistence::UserPersistence::get_all_users_apps();
// Convert user apps to marketplace products and add them
// Collect products to register in MockDataService
let mut products_to_register = Vec::new();
for app in user_apps {
let marketplace_product = create_marketplace_product_from_app(&app);
let availability = marketplace_product.availability.clone();
// Check if product already exists in all_applications and update or add accordingly
if let Some(existing_index) = all_applications.iter().position(|p| p.id == marketplace_product.id) {
// Update existing product with new availability status
all_applications[existing_index] = marketplace_product.clone();
products_to_register.push((marketplace_product, app.name.clone()));
} else {
// Add new product
all_applications.push(marketplace_product.clone());
products_to_register.push((marketplace_product, app.name.clone()));
}
}
// Product registration is now handled through persistent user data
// ProductService automatically aggregates products from user-owned data
// Cart functionality works with persistent product catalog
// This approach ensures proper separation:
// - Dashboard: Shows only current user's apps from their persistent data
// - Marketplace: Shows all apps from all users' persistent data (global aggregation)
// Convert prices to user's currency
let products_with_prices = match product_service.get_products_with_converted_prices(
&all_applications,
&display_currency,
) {
Ok(converted) => converted.into_iter()
.map(|(product, price)| {
let formatted_price = currency_service.format_price(
price.display_amount,
&price.display_currency,
).unwrap_or_else(|_| format!("{} {}", price.display_amount, price.display_currency));
serde_json::json!({
"product": product,
"price": price,
"formatted_price": formatted_price
})
})
.collect::<Vec<_>>(),
Err(_) => Vec::new(),
};
ctx.insert("application_products", &products_with_prices);
ctx.insert("currencies", &currency_service.get_currency_display_info());
ctx.insert("user_currency", &display_currency);
// Calculate updated pagination data including user apps
let total_applications = all_applications.len();
let updated_total_pages = (total_applications + page_size - 1) / page_size;
// Add pagination data
ctx.insert("pagination", &serde_json::json!({
"current_page": search_result.page,
"total_pages": updated_total_pages,
"total_count": total_applications,
"page_size": search_result.page_size,
"has_previous": search_result.page > 0,
"has_next": search_result.page < updated_total_pages.saturating_sub(1),
"previous_page": search_result.page.saturating_sub(1),
"next_page": search_result.page + 1
}));
// Add current filters to context
ctx.insert("current_filters", &query.into_inner());
// Add user data to context
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
if let Ok(user) = serde_json::from_str::<crate::models::user::User>(&user_json) {
ctx.insert("user", &user);
}
}
render_template(&tmpl, "marketplace/applications.html", &ctx)
}
/// Renders the services marketplace page
pub async fn services(tmpl: web::Data<Tera>, session: Session, query: web::Query<std::collections::HashMap<String, String>>) -> Result<impl Responder> {
let product_service = ProductService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let currency_service = CurrencyService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("marketplace")
.build();
ctx.insert("active_section", "services");
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Get user's preferred currency
let display_currency = currency_service.get_user_preferred_currency(&session);
// Parse pagination and filter parameters
let page = query.get("page")
.and_then(|p| p.parse::<usize>().ok())
.unwrap_or(0);
let page_size = 12; // Products per page
// Build search criteria from query parameters
// Accept both "service" and "application" categories for backward compatibility
let mut criteria = crate::services::product::ProductSearchCriteria::new();
// Note: Don't filter by category here - let all products through and filter in display
// Add filters from query parameters
if let Some(service_type) = query.get("service_type") {
if !service_type.is_empty() {
criteria = criteria.with_attribute("service_type".to_string(), serde_json::Value::String(service_type.clone()));
}
}
if let Some(provider_rating) = query.get("provider_rating") {
if !provider_rating.is_empty() {
criteria = criteria.with_attribute("provider_rating".to_string(), serde_json::Value::String(provider_rating.clone()));
}
}
let mut min_price_val = None;
let mut max_price_val = None;
if let Some(min_price) = query.get("min_price") {
if let Ok(price) = min_price.parse::<rust_decimal::Decimal>() {
min_price_val = Some(price);
}
}
if let Some(max_price) = query.get("max_price") {
if let Ok(price) = max_price.parse::<rust_decimal::Decimal>() {
max_price_val = Some(price);
}
}
if min_price_val.is_some() || max_price_val.is_some() {
criteria = criteria.with_price_range(min_price_val, max_price_val);
}
// Search products with pagination
let search_result = product_service.search_products_advanced(&criteria, page, page_size);
// TEMP DEBUG: Check if we're actually getting all users' products
let all_raw_products = product_service.get_all_products();
let service_products: Vec<_> = all_raw_products.iter()
.filter(|p| p.category_id == "service")
.collect();
println!("🔍 MARKETPLACE: Found {} total service products from all users", service_products.len());
// Debug: Log all product IDs found
for product in &search_result.products {
}
// Filter for service-related products (both "service" and "application" categories)
// This handles backward compatibility with existing products
let all_services: Vec<_> = search_result.products.into_iter()
.filter(|product| {
let is_service_category = product.category_id == "service" || product.category_id == "application";
if is_service_category {
println!("🔍 MARKETPLACE: Including product {} (category: {})", product.name, product.category_id);
}
is_service_category
})
.collect();
println!("🔍 MARKETPLACE: Filtered to {} service/application products", all_services.len());
// Convert prices to user's currency
let products_with_prices = match product_service.get_products_with_converted_prices(
&all_services,
&display_currency,
) {
Ok(converted) => converted.into_iter()
.map(|(product, price)| {
let formatted_price = currency_service.format_price(
price.display_amount,
&price.display_currency,
).unwrap_or_else(|_| format!("{} {}", price.display_amount, price.display_currency));
serde_json::json!({
"product": product,
"price": price,
"formatted_price": formatted_price
})
})
.collect::<Vec<_>>(),
Err(_) => Vec::new(),
};
ctx.insert("service_products", &products_with_prices);
ctx.insert("currencies", &currency_service.get_currency_display_info());
ctx.insert("user_currency", &display_currency);
// Calculate updated pagination data including user services
let total_services = all_services.len();
let updated_total_pages = (total_services + page_size - 1) / page_size;
// Add pagination data
ctx.insert("pagination", &serde_json::json!({
"current_page": search_result.page,
"total_pages": updated_total_pages,
"total_count": total_services,
"page_size": search_result.page_size,
"has_previous": search_result.page > 0,
"has_next": search_result.page < updated_total_pages.saturating_sub(1),
"previous_page": search_result.page.saturating_sub(1),
"next_page": search_result.page + 1
}));
// Add current filters to context
ctx.insert("current_filters", &query.into_inner());
// Add user data to context
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
if let Ok(user) = serde_json::from_str::<crate::models::user::User>(&user_json) {
ctx.insert("user", &user);
}
}
render_template(&tmpl, "marketplace/services.html", &ctx)
}
/// Renders the marketplace statistics page
pub async fn statistics(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("marketplace")
.build();
ctx.insert("active_section", "statistics");
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Add user data to context
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
if let Ok(user) = serde_json::from_str::<crate::models::user::User>(&user_json) {
ctx.insert("user", &user);
}
}
render_template(&tmpl, "marketplace/statistics.html", &ctx)
}
/// Show slice rental form with deployment options
pub async fn show_slice_rental_form(
tmpl: web::Data<Tera>,
session: Session,
path: web::Path<(String, String, String)> // farmer_email, node_id, combination_id
) -> Result<impl Responder> {
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("marketplace")
.build();
ctx.insert("active_section", "compute");
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Add user data to context
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
if let Ok(user) = serde_json::from_str::<crate::models::user::User>(&user_json) {
ctx.insert("user", &user);
}
}
let (farmer_email, node_id, combination_id) = path.into_inner();
// Get slice details for the form
let node_marketplace_service = NodeMarketplaceService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let slice_combinations = node_marketplace_service.get_all_slice_combinations();
if !slice_combinations.is_empty() {
// Find the specific slice combination by checking product attributes
if let Some(slice_product) = slice_combinations.iter().find(|p| {
// Check if this product matches the requested slice
if let (Some(farmer_attr), Some(node_attr), Some(combo_attr)) = (
p.attributes.get("farmer_email"),
p.attributes.get("node_id"),
p.attributes.get("combination_id")
) {
farmer_attr.value.as_str() == Some(&farmer_email) &&
node_attr.value.as_str() == Some(&node_id) &&
combo_attr.value.as_str() == Some(&combination_id)
} else {
false
}
}) {
// Extract slice information from product attributes
let mut slice_info = std::collections::HashMap::new();
if let Some(specs) = slice_product.attributes.get("slice_specs") {
if let Some(cpu) = specs.value.get("cpu_cores") {
slice_info.insert("cpu_cores", cpu.clone());
}
if let Some(memory) = specs.value.get("memory_gb") {
slice_info.insert("memory_gb", memory.clone());
}
if let Some(storage) = specs.value.get("storage_gb") {
slice_info.insert("storage_gb", storage.clone());
}
}
if let Some(availability) = slice_product.attributes.get("availability") {
if let Some(quantity) = availability.value.get("quantity_available") {
slice_info.insert("available_quantity", quantity.clone());
}
}
slice_info.insert("price_per_hour", serde_json::Value::String(slice_product.base_price.to_string()));
slice_info.insert("node_id", serde_json::Value::String(node_id.clone()));
slice_info.insert("farmer_email", serde_json::Value::String(farmer_email.clone()));
slice_info.insert("combination_id", serde_json::Value::String(combination_id.clone()));
ctx.insert("slice", &slice_info);
ctx.insert("farmer_email", &farmer_email);
ctx.insert("node_id", &node_id);
ctx.insert("combination_id", &combination_id);
}
}
render_template(&tmpl, "marketplace/slice_rental_form.html", &ctx)
}
/// Handle slice rental form submission with enhanced deployment options
pub async fn process_slice_rental(
session: Session,
form: web::Form<SliceRentalForm>
) -> Result<impl Responder> {
// Check if user is logged in
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"message": "Please log in to rent slices"
})).build();
}
};
// Validate deployment type
if !matches!(form.deployment_type.as_str(), "vm" | "kubernetes") {
return ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "Invalid deployment type. Must be 'vm' or 'kubernetes'"
})).build();
}
// Validate deployment-specific fields
let deployment_name = match form.deployment_type.as_str() {
"vm" => {
if form.vm_name.is_none() {
return ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "VM name is required for VM deployments"
})).build();
}
form.vm_name.as_ref().unwrap().clone()
},
"kubernetes" => {
if form.cluster_name.is_none() || form.k8s_masters.is_none() || form.k8s_workers.is_none() {
return ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "Cluster name, masters count, and workers count are required for Kubernetes deployments"
})).build();
}
form.cluster_name.as_ref().unwrap().clone()
},
_ => unreachable!()
};
// Create deployment configuration
let deployment_config = SliceDeploymentConfig {
deployment_id: uuid::Uuid::new_v4().to_string(),
deployment_type: match form.deployment_type.as_str() {
"vm" => DeploymentType::VM {
name: form.vm_name.clone().unwrap_or_default(),
os: form.vm_os.clone().unwrap_or_else(|| "ubuntu".to_string()),
ssh_key: form.vm_ssh_key.clone(),
},
"kubernetes" => DeploymentType::Kubernetes {
cluster_name: form.cluster_name.clone().unwrap_or_default(),
masters: form.k8s_masters.unwrap_or(1),
workers: form.k8s_workers.unwrap_or(1),
version: form.k8s_version.clone().unwrap_or_else(|| "1.29".to_string()),
network_plugin: form.k8s_network_plugin.clone().unwrap_or_else(|| "flannel".to_string()),
},
_ => unreachable!()
},
slice_specs: SliceSpecs {
cpu_cores: 0, // Will be filled by service
memory_gb: 0, // Will be filled by service
storage_gb: 0, // Will be filled by service
quantity: form.quantity,
},
network_config: NetworkConfig {
public_ip: true,
private_network: "10.0.0.0/24".to_string(),
ports: vec![22, 80, 443],
},
security_config: SecurityConfig {
firewall_enabled: true,
ssh_access: true,
monitoring: form.monitoring_enabled.unwrap_or(false),
backup: form.backup_enabled.unwrap_or(false),
},
created_at: chrono::Utc::now(),
status: DeploymentStatus::Pending,
};
// Build slice rental service
let slice_rental_service = SliceRentalService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
// Convert deployment config to JSON for the service
let deployment_config_json = serde_json::to_value(&deployment_config)
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
// Attempt to rent the slice with deployment options
match slice_rental_service.rent_slice_combination_with_deployment(
&user_email,
&form.farmer_email,
&form.node_id,
&form.combination_id,
form.quantity,
form.rental_duration_hours,
&form.deployment_type,
&deployment_name,
Some(deployment_config_json),
) {
Ok(rental) => {
// Create user activity record
let activity = crate::models::user::UserActivity {
id: uuid::Uuid::new_v4().to_string(),
user_email: user_email.clone(),
activity_type: crate::models::user::ActivityType::SliceAllocated,
description: format!("Rented {} slice(s) for {} deployment '{}'",
form.quantity, form.deployment_type, deployment_name),
timestamp: chrono::Utc::now(),
metadata: Some({
let mut meta = std::collections::HashMap::new();
meta.insert("rental_id".to_string(), serde_json::Value::String(rental.rental_id.clone()));
meta.insert("deployment_type".to_string(), serde_json::Value::String(form.deployment_type.clone()));
meta.insert("deployment_name".to_string(), serde_json::Value::String(deployment_name.clone()));
meta.insert("quantity".to_string(), serde_json::Value::Number(serde_json::Number::from(form.quantity)));
meta.insert("farmer_email".to_string(), serde_json::Value::String(form.farmer_email.clone()));
if form.deployment_type == "kubernetes" {
meta.insert("k8s_masters".to_string(), serde_json::Value::Number(serde_json::Number::from(form.k8s_masters.unwrap_or(1))));
meta.insert("k8s_workers".to_string(), serde_json::Value::Number(serde_json::Number::from(form.k8s_workers.unwrap_or(1))));
}
serde_json::Value::Object(meta.into_iter().collect())
}),
category: "slice_rental".to_string(),
importance: crate::models::user::ActivityImportance::High,
ip_address: None,
user_agent: None,
session_id: None,
};
// Add activity to user's record
if let Ok(user_service) = crate::services::user_service::UserService::builder().build() {
let _ = user_service.add_user_activity(&user_email, activity);
}
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Slice rental successful! {} deployment '{}' is being set up.",
form.deployment_type.to_uppercase(), deployment_name),
"rental_id": rental.rental_id,
"total_cost": rental.total_cost,
"deployment_type": form.deployment_type,
"deployment_name": deployment_name,
"redirect_url": "/dashboard/user"
})).build()
},
Err(error) => {
ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": error
})).build()
}
}
}
/// Handle slice rental requests with deployment options (legacy method)
pub async fn rent_slice(
session: Session,
form: web::Form<SliceRentalRequest>
) -> Result<impl Responder> {
// Check if user is logged in
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"message": "Please log in to rent slices"
})).build();
}
};
// Validate deployment type
if !matches!(form.deployment_type.as_str(), "vm" | "kubernetes") {
return ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "Invalid deployment type. Must be 'vm' or 'kubernetes'"
})).build();
}
// Build slice rental service
let slice_rental_service = SliceRentalService::builder()
.build()
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
// Attempt to rent the slice with deployment options
match slice_rental_service.rent_slice_combination_with_deployment(
&user_email,
&form.farmer_email,
&form.node_id,
&form.combination_id,
form.quantity,
form.rental_duration_hours,
&form.deployment_type,
&form.deployment_name,
form.deployment_config.clone(),
) {
Ok(rental) => {
// Create user activity record
let activity = crate::models::user::UserActivity {
id: uuid::Uuid::new_v4().to_string(),
user_email: user_email.clone(),
activity_type: crate::models::user::ActivityType::SliceAllocated,
description: format!("Rented {} slice(s) for {} deployment '{}'",
form.quantity, form.deployment_type, form.deployment_name),
timestamp: chrono::Utc::now(),
metadata: Some({
let mut meta = std::collections::HashMap::new();
meta.insert("rental_id".to_string(), serde_json::Value::String(rental.rental_id.clone()));
meta.insert("deployment_type".to_string(), serde_json::Value::String(form.deployment_type.clone()));
meta.insert("deployment_name".to_string(), serde_json::Value::String(form.deployment_name.clone()));
meta.insert("quantity".to_string(), serde_json::Value::Number(serde_json::Number::from(form.quantity)));
meta.insert("farmer_email".to_string(), serde_json::Value::String(form.farmer_email.clone()));
serde_json::Value::Object(meta.into_iter().collect())
}),
category: "slice_rental".to_string(),
importance: crate::models::user::ActivityImportance::High,
ip_address: None,
user_agent: None,
session_id: None,
};
// Add activity to user's record
if let Ok(user_service) = crate::services::user_service::UserService::builder().build() {
let _ = user_service.add_user_activity(&user_email, activity);
}
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Slice rental successful! {} deployment '{}' is being set up.",
form.deployment_type.to_uppercase(), form.deployment_name),
"rental_id": rental.rental_id,
"total_cost": rental.total_cost,
"deployment_type": form.deployment_type,
"deployment_name": form.deployment_name
})).build()
},
Err(error) => {
ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": error
})).build()
}
}
}
}
/// Request structure for slice rental with deployment options
#[derive(serde::Deserialize)]
pub struct SliceRentalRequest {
pub farmer_email: String,
pub node_id: String,
pub combination_id: String,
pub quantity: u32,
pub rental_duration_hours: u32,
pub deployment_type: String, // "vm" or "kubernetes"
pub deployment_name: String,
pub deployment_config: Option<serde_json::Value>, // Additional config for K8s clusters
}
/// Helper function to convert a Service to a marketplace Product
pub fn create_marketplace_product_from_service(service: &crate::models::user::Service) -> crate::models::product::Product {
use crate::models::product::{Product, ProductAttribute, ProductAvailability, ProductMetadata, AttributeType};
let availability = if service.status == "Active" {
ProductAvailability::Available
} else {
ProductAvailability::Unavailable
};
let metadata = ProductMetadata {
tags: vec![service.category.clone(), "user-created".to_string()],
location: Some("Remote".to_string()),
rating: Some(service.rating),
review_count: 0,
featured: false,
last_updated: chrono::Utc::now(),
visibility: crate::models::product::ProductVisibility::Public,
seo_keywords: Vec::new(),
custom_fields: std::collections::HashMap::new(),
};
Product::builder()
.id(&service.id)
.name(&service.name)
.description(&service.description)
.category_id("service")
.base_price(rust_decimal::Decimal::from(service.price_per_hour_usd))
.base_currency("USD")
.provider_id("user-service-provider")
.provider_name("Service Provider")
.availability(availability)
.metadata(metadata)
.add_attribute("service_type", ProductAttribute {
key: "service_type".to_string(),
value: serde_json::Value::String(service.category.clone()),
attribute_type: AttributeType::Text,
is_searchable: true,
is_filterable: true,
display_order: Some(1),
})
.add_attribute("provider_rating", ProductAttribute {
key: "provider_rating".to_string(),
value: serde_json::Value::Number(serde_json::Number::from_f64(service.rating as f64).unwrap_or(serde_json::Number::from(0))),
attribute_type: AttributeType::Number,
is_searchable: false,
is_filterable: true,
display_order: Some(2),
})
.add_attribute("experience_level", ProductAttribute {
key: "experience_level".to_string(),
value: serde_json::Value::String("intermediate".to_string()),
attribute_type: AttributeType::Text,
is_searchable: true,
is_filterable: true,
display_order: Some(3),
})
.add_attribute("response_time", ProductAttribute {
key: "response_time".to_string(),
value: serde_json::Value::String("24 hours".to_string()),
attribute_type: AttributeType::Text,
is_searchable: true,
is_filterable: false,
display_order: Some(4),
})
.build()
.unwrap()
}
/// Helper function to convert a PublishedApp to a marketplace Product
pub fn create_marketplace_product_from_app(app: &crate::models::user::PublishedApp) -> crate::models::product::Product {
use crate::models::product::{Product, ProductAttribute, ProductAvailability, ProductMetadata, AttributeType};
let availability = if app.status == "Active" {
ProductAvailability::Available
} else {
ProductAvailability::Unavailable
};
let metadata = ProductMetadata {
tags: vec![app.category.clone(), "user-created".to_string()],
location: Some("Cloud".to_string()),
rating: Some(app.rating),
review_count: 0,
featured: false,
last_updated: chrono::Utc::now(),
visibility: crate::models::product::ProductVisibility::Public,
seo_keywords: Vec::new(),
custom_fields: std::collections::HashMap::new(),
};
Product::builder()
.id(&app.id)
.name(&app.name)
.description(format!("Application: {} (Version {})", app.name, app.version))
.category_id("application")
.base_price(rust_decimal::Decimal::from(app.monthly_revenue_usd.max(rust_decimal::Decimal::ONE))) // Use monthly revenue as base price, minimum $1
.base_currency("USD")
.provider_id("user-app-provider")
.provider_name("App Provider")
.availability(availability)
.metadata(metadata)
.add_attribute("app_type", ProductAttribute {
key: "app_type".to_string(),
value: serde_json::Value::String(app.category.clone()),
attribute_type: AttributeType::Text,
is_searchable: true,
is_filterable: true,
display_order: Some(1),
})
.add_attribute("deployment_type", ProductAttribute {
key: "deployment_type".to_string(),
value: serde_json::Value::String("Container".to_string()), // Default deployment type
attribute_type: AttributeType::Text,
is_searchable: true,
is_filterable: true,
display_order: Some(2),
})
.add_attribute("version", ProductAttribute {
key: "version".to_string(),
value: serde_json::Value::String(app.version.clone()),
attribute_type: AttributeType::Text,
is_searchable: true,
is_filterable: false,
display_order: Some(3),
})
.add_attribute("deployments", ProductAttribute {
key: "deployments".to_string(),
value: serde_json::Value::Number(serde_json::Number::from(app.deployments)),
attribute_type: AttributeType::Number,
is_searchable: false,
is_filterable: false,
display_order: Some(4),
})
.build()
.unwrap()
}
/// Helper function to reconstruct a Service from JSON data (fallback mechanism)
fn reconstruct_service_from_json(service_value: &serde_json::Value) -> Result<crate::models::user::Service, Box<dyn std::error::Error>> {
let price_per_hour = service_value.get("price_per_hour")
.and_then(|v| v.as_i64())
.unwrap_or(0) as i32;
let service = crate::models::user::Service {
id: service_value.get("id")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string(),
name: service_value.get("name")
.and_then(|v| v.as_str())
.unwrap_or("Unknown Service")
.to_string(),
category: service_value.get("category")
.and_then(|v| v.as_str())
.unwrap_or("General")
.to_string(),
description: service_value.get("description")
.and_then(|v| v.as_str())
.unwrap_or("No description available")
.to_string(),
price_usd: rust_decimal::Decimal::new(price_per_hour as i64, 0),
hourly_rate_usd: Some(rust_decimal::Decimal::new(price_per_hour as i64, 0)),
availability: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
price_per_hour_usd: price_per_hour,
status: service_value.get("status")
.and_then(|v| v.as_str())
.unwrap_or("Active")
.to_string(),
clients: service_value.get("clients")
.and_then(|v| v.as_i64())
.unwrap_or(0) as i32,
rating: service_value.get("rating")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32,
total_hours: Some(service_value.get("total_hours")
.and_then(|v| v.as_i64())
.unwrap_or(0) as i32),
};
Ok(service)
}
/// Handle slice rental requests from marketplace
pub async fn rent_slice(
form: web::Form<SliceRentalForm>,
session: Session,
) -> Result<impl Responder> {
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"message": "User not authenticated"
})).build();
}
};
// Build slice rental service
let slice_rental_service = match SliceRentalService::builder().build() {
Ok(service) => service,
Err(e) => {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Service initialization failed"
})).build();
}
};
// Validate deployment configuration
if form.deployment_type == "kubernetes" {
if form.k8s_masters.is_none() || form.k8s_workers.is_none() || form.cluster_name.is_none() {
return ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "Kubernetes deployment requires masters, workers, and cluster name"
})).build();
}
}
// Attempt to rent the slice combination
match slice_rental_service.rent_slice_combination(
&user_email,
&form.farmer_email,
&form.node_id,
&form.combination_id,
form.quantity,
form.rental_duration_hours,
) {
Ok(rental) => {
// Create deployment metadata based on type
let mut deployment_metadata = std::collections::HashMap::new();
deployment_metadata.insert("deployment_type".to_string(), serde_json::Value::String(form.deployment_type.clone()));
deployment_metadata.insert("rental_duration_hours".to_string(), serde_json::Value::Number(serde_json::Number::from(form.rental_duration_hours)));
if form.deployment_type == "kubernetes" {
deployment_metadata.insert("k8s_masters".to_string(), serde_json::Value::Number(serde_json::Number::from(form.k8s_masters.unwrap_or(1))));
deployment_metadata.insert("k8s_workers".to_string(), serde_json::Value::Number(serde_json::Number::from(form.k8s_workers.unwrap_or(2))));
deployment_metadata.insert("cluster_name".to_string(), serde_json::Value::String(form.cluster_name.clone().unwrap_or_else(|| format!("cluster-{}", Uuid::new_v4().to_string()[..8].to_string()))));
}
// Add user activity for slice rental
if let Ok(user_service) = crate::services::user_service::UserService::builder().build() {
let activity = crate::models::user::UserActivity {
id: Uuid::new_v4().to_string(),
user_email: user_email.clone(),
activity_type: crate::models::user::ActivityType::SliceAllocated,
description: format!("Rented {} slice(s) for {} deployment", form.quantity, form.deployment_type),
timestamp: Utc::now(),
metadata: Some(serde_json::Value::Object(deployment_metadata.clone().into_iter().collect())),
category: "Slice Rental".to_string(),
importance: crate::models::user::ActivityImportance::High,
ip_address: None,
user_agent: None,
session_id: None,
};
if let Err(e) = user_service.add_user_activity(&user_email, activity) {
}
/// Create a new slice assignment for deployment
pub async fn create_slice_assignment(
session: Session,
assignment_data: web::Json<serde_json::Value>,
) -> Result<impl Responder> {
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"message": "User not authenticated"
})).build();
}
};
// Build slice assignment service
let slice_assignment_service = match SliceAssignmentService::builder().build() {
Ok(service) => service,
Err(e) => {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Service initialization failed"
})).build();
}
};
// Extract assignment details from request
let assignment_request = match assignment_data.into_inner() {
serde_json::Value::Object(map) => map,
_ => {
return ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "Invalid assignment data format"
})).build();
}
};
// Validate required fields
let slice_id = match assignment_request.get("slice_id").and_then(|v| v.as_str()) {
Some(id) => id.to_string(),
None => {
return ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "slice_id is required"
})).build();
}
};
let deployment_type = match assignment_request.get("deployment_type").and_then(|v| v.as_str()) {
Some(dt) => dt.to_string(),
None => {
return ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "deployment_type is required"
})).build();
}
};
// Create SliceAssignmentRequest from the data
let assignment_request_obj = SliceAssignmentRequest {
user_email: user_email.clone(),
farmer_email: assignment_request.get("farmer_email")
.and_then(|v| v.as_str())
.unwrap_or("unknown@example.com")
.to_string(),
node_id: assignment_request.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or(&slice_id)
.to_string(),
combination_id: slice_id.clone(),
quantity: assignment_request.get("quantity")
.and_then(|v| v.as_u64())
.unwrap_or(1) as u32,
rental_duration_hours: assignment_request.get("rental_duration_hours")
.and_then(|v| v.as_u64())
.unwrap_or(24) as u32,
total_cost: rust_decimal::Decimal::from(
assignment_request.get("total_cost")
.and_then(|v| v.as_i64())
.unwrap_or(50) as i32
),
deployment_config: serde_json::from_value(serde_json::Value::Object(assignment_request.clone()))
.unwrap_or_else(|_| DeploymentConfiguration {
deployment_type: SliceDeploymentType::IndividualVM {
vm_configs: vec![VMConfiguration {
vm_name: "default-vm".to_string(),
os_image: "ubuntu-22.04".to_string(),
ssh_key: None,
slice_count: 1,
auto_scaling: false,
custom_startup_script: None,
}],
},
assignment_mode: AssignmentMode::IndividualVMs,
network_config: NetworkConfiguration {
public_ip_required: false,
private_network_cidr: Some("10.0.0.0/24".to_string()),
exposed_ports: vec![],
load_balancer_enabled: false,
},
security_config: SecurityConfiguration {
firewall_enabled: true,
ssh_access_enabled: true,
vpn_access_enabled: false,
encryption_at_rest: true,
encryption_in_transit: true,
},
monitoring_enabled: true,
backup_enabled: false,
}),
};
// Create the assignment
match slice_assignment_service.create_assignment(assignment_request_obj) {
Ok(assignment) => {
// Add user activity
if let Ok(user_service) = crate::services::user_service::UserService::builder().build() {
let activity = crate::models::user::UserActivity {
id: Uuid::new_v4().to_string(),
user_email: user_email.clone(),
activity_type: crate::models::user::ActivityType::SliceAllocated,
description: format!("Created {} deployment assignment for slice {}", deployment_type, slice_id),
timestamp: Utc::now(),
metadata: Some(serde_json::Value::Object(assignment_request.into_iter().collect())),
category: "Slice Assignment".to_string(),
importance: crate::models::user::ActivityImportance::High,
ip_address: None,
user_agent: None,
session_id: None,
};
let _ = user_service.add_user_activity(&user_email, activity);
}
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"assignment": assignment,
"message": "Slice assignment created successfully"
})).build()
}
Err(e) => {
ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": format!("Assignment creation failed: {}", e)
})).build()
}
}
}
/// Get all slice assignments for the authenticated user
pub async fn get_slice_assignments(session: Session) -> Result<impl Responder> {
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"message": "User not authenticated"
})).build();
}
};
// Build slice assignment service
let slice_assignment_service = match SliceAssignmentService::builder().build() {
Ok(service) => service,
Err(e) => {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Service initialization failed"
})).build();
}
};
// Get user's assignments
match slice_assignment_service.get_user_assignments(&user_email) {
Ok(assignments) => {
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"assignments": assignments,
"count": assignments.len()
})).build()
}
Err(e) => {
ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": format!("Failed to retrieve assignments: {}", e)
})).build()
}
}
}
/// Get details for a specific slice assignment
pub async fn get_slice_assignment_details(
session: Session,
path: web::Path<String>,
) -> Result<impl Responder> {
let assignment_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"message": "User not authenticated"
})).build();
}
};
// Build slice assignment service
let slice_assignment_service = match SliceAssignmentService::builder().build() {
Ok(service) => service,
Err(e) => {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Service initialization failed"
})).build();
}
};
// Get assignment details
match slice_assignment_service.get_assignment_details(&assignment_id, &user_email) {
Ok(Some(assignment)) => {
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"assignment": assignment
})).build()
}
Ok(None) => {
ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"message": "Assignment not found or access denied"
})).build()
}
Err(e) => {
ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": format!("Failed to retrieve assignment details: {}", e)
})).build()
}
}
}
/// Update a slice assignment configuration
pub async fn update_slice_assignment(
session: Session,
path: web::Path<String>,
update_data: web::Json<serde_json::Value>,
) -> Result<impl Responder> {
let assignment_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"message": "User not authenticated"
})).build();
}
};
// Build slice assignment service
let slice_assignment_service = match SliceAssignmentService::builder().build() {
Ok(service) => service,
Err(e) => {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Service initialization failed"
})).build();
}
};
// Extract update data
let update_config = match update_data.into_inner() {
serde_json::Value::Object(map) => map,
_ => {
return ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "Invalid update data format"
})).build();
}
};
// Update the assignment
match slice_assignment_service.update_assignment(&assignment_id, &user_email, update_config.clone().into_iter().collect()) {
Ok(updated_assignment) => {
// Add user activity
if let Ok(user_service) = crate::services::user_service::UserService::builder().build() {
let activity = crate::models::user::UserActivity {
id: Uuid::new_v4().to_string(),
user_email: user_email.clone(),
activity_type: crate::models::user::ActivityType::SliceAllocated,
description: format!("Updated slice assignment configuration for {}", assignment_id),
timestamp: Utc::now(),
metadata: Some(serde_json::Value::Object(update_config.into_iter().collect())),
category: "Slice Assignment".to_string(),
importance: crate::models::user::ActivityImportance::Medium,
ip_address: None,
user_agent: None,
session_id: None,
};
let _ = user_service.add_user_activity(&user_email, activity);
}
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"assignment": updated_assignment,
"message": "Assignment updated successfully"
})).build()
}
Err(e) => {
ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": format!("Assignment update failed: {}", e)
})).build()
}
}
}
/// Delete a slice assignment
pub async fn delete_slice_assignment(
session: Session,
path: web::Path<String>,
) -> Result<impl Responder> {
let assignment_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"message": "User not authenticated"
})).build();
}
};
// Build slice assignment service
let slice_assignment_service = match SliceAssignmentService::builder().build() {
Ok(service) => service,
Err(e) => {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Service initialization failed"
})).build();
}
};
// Delete the assignment
match slice_assignment_service.delete_assignment(&assignment_id, &user_email) {
Ok(()) => {
// Add user activity
if let Ok(user_service) = crate::services::user_service::UserService::builder().build() {
let activity = crate::models::user::UserActivity {
id: Uuid::new_v4().to_string(),
user_email: user_email.clone(),
activity_type: crate::models::user::ActivityType::SliceAllocated,
description: format!("Deleted slice assignment {}", assignment_id),
timestamp: Utc::now(),
metadata: Some(serde_json::Value::Object(std::collections::HashMap::new().into_iter().collect())),
category: "Slice Assignment".to_string(),
importance: crate::models::user::ActivityImportance::Medium,
ip_address: None,
user_agent: None,
session_id: None,
};
let _ = user_service.add_user_activity(&user_email, activity);
}
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Assignment deleted successfully"
})).build()
}
Err(e) => {
ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": format!("Assignment deletion failed: {}", e)
})).build()
}
}
}
/// Deploy a slice assignment (start the actual deployment)
pub async fn deploy_slice_assignment(
session: Session,
path: web::Path<String>,
) -> Result<impl Responder> {
let assignment_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"message": "User not authenticated"
})).build();
}
};
// Build slice assignment service
let slice_assignment_service = match SliceAssignmentService::builder().build() {
Ok(service) => service,
Err(e) => {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Service initialization failed"
})).build();
}
};
// Deploy the assignment
match slice_assignment_service.deploy_assignment(&assignment_id, &user_email) {
Ok(deployment_info) => {
// Add user activity
if let Ok(user_service) = crate::services::user_service::UserService::builder().build() {
let activity = crate::models::user::UserActivity {
id: Uuid::new_v4().to_string(),
user_email: user_email.clone(),
activity_type: crate::models::user::ActivityType::SliceAllocated,
description: format!("Deployed slice assignment {}", assignment_id),
timestamp: Utc::now(),
metadata: Some(serde_json::Value::Object(std::collections::HashMap::from([
("assignment_id".to_string(), serde_json::Value::String(assignment_id.clone())),
("deployment_info".to_string(), serde_json::to_value(&deployment_info).unwrap_or_default()),
]).into_iter().collect())),
category: "Slice Deployment".to_string(),
importance: crate::models::user::ActivityImportance::High,
ip_address: None,
user_agent: None,
session_id: None,
};
let _ = user_service.add_user_activity(&user_email, activity);
}
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"deployment": deployment_info,
"message": "Assignment deployed successfully"
})).build()
}
Err(e) => {
ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": format!("Deployment failed: {}", e)
})).build()
}
}
}
}
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Slice rental successful",
"rental_id": rental.rental_id,
"deployment_type": form.deployment_type,
"rental_duration_hours": form.rental_duration_hours,
"metadata": deployment_metadata
})).build()
}
Err(e) => {
ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": format!("Rental failed: {}", e)
})).build()
}
}
}