2020 lines
94 KiB
Rust
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", ¤cy_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", ¤cy_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", ¤cy_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", ¤cy_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", ¤cy_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", ¤cy_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()
|
|
}
|
|
}
|
|
} |