Files
projectmycelium/src/controllers/dashboard.rs

7800 lines
340 KiB
Rust

use tera::Tera;
use actix_web::{web, Result, Responder};
use crate::utils::response_builder::ResponseBuilder;
use crate::utils::render_template;
use crate::config::get_app_config;
use actix_session::Session;
use std::str::FromStr;
use crate::models::user::User;
use crate::models::builders::NodeStakingOptionsBuilder;
use crate::services::session_manager::SessionManager;
use rust_decimal::prelude::ToPrimitive;
use crate::services::user_persistence::UserPersistence;
use crate::services::user_service::UserService;
use serde::Deserialize;
use rust_decimal::Decimal;
use uuid::Uuid;
use chrono::Utc;
use crate::utils::DataCleanup;
use bcrypt::verify;
use std::time::Instant;
/// Controller for handling dashboard-related routes
pub struct DashboardController;
impl DashboardController {
/// Check if user is authenticated, redirect to register if not
fn check_authentication(session: &Session) -> Result<(), actix_web::HttpResponse> {
match session.get::<String>("user") {
Ok(Some(_)) => Ok(()),
_ => Err(ResponseBuilder::redirect("/register").build()?)
}
}
/// Helper function to load user with persistent data from session
fn load_user_with_persistent_data(session: &Session) -> Option<User> {
// Get basic user data
let user_json = session.get::<String>("user").ok()??;
let mut user: User = serde_json::from_str(&user_json).ok()?;
// Load fresh mock data based on user email
let user_email = session.get::<String>("user_email").ok()??;
// Load persistent user data instead of mock data
if let Some(user_data) = crate::services::user_persistence::UserPersistence::load_user_data(&user_email) {
// Update user object with persistent data
user.name = user_data.name.unwrap_or_else(|| user_email.clone());
} else {
// Create default user data if none exists
let _default_data = crate::services::user_persistence::UserPersistence::create_default_user_data(&user_email);
}
// Apply session data using SessionManager
if let Some(session_data) = SessionManager::load_user_session_data(session) {
SessionManager::apply_session_to_user(&mut user, &session_data);
}
// PHASE 1 FIX: Enhanced persistent data loading with better consistency
if let Some(persistent_data) = UserPersistence::load_user_data(&user_email) {
for service in &persistent_data.services {
}
if let Some(name) = persistent_data.name {
user.name = name;
}
if let Some(country) = persistent_data.country {
user.country = Some(country.clone());
}
if let Some(timezone) = persistent_data.timezone {
user.timezone = Some(timezone.clone());
}
// Apply persistent services data - removed mock_data dependency
// Service provider data now comes from UserPersistence directly
// All data is accessible through persistent_data without mock layer
}
// All user data is now accessible through persistent_data without mock layer
// Function clean end
Some(user)
}
/// Helper function to count deployments across all users for a specific application provider
fn count_cross_user_deployments(application_provider_email: &str) -> std::collections::HashMap<String, i32> {
use std::collections::HashMap;
let mut deployment_counts: HashMap<String, i32> = HashMap::new();
// Get all user data files
let user_data_dir = std::path::Path::new("user_data");
if !user_data_dir.exists() {
return deployment_counts;
}
// Read all user files
if let Ok(entries) = std::fs::read_dir(user_data_dir) {
for entry in entries.flatten() {
if let Some(file_name) = entry.file_name().to_str() {
if file_name.ends_with(".json")
&& file_name.contains("_at_")
&& !file_name.contains("_cart")
&& file_name != "session_data.json"
{
let file_path = entry.path();
// Read and parse user data
if let Ok(content) = std::fs::read_to_string(&file_path) {
if let Ok(user_data) = serde_json::from_str::<crate::services::user_persistence::UserPersistentData>(&content) {
// Count deployments for this app provider's apps
for deployment in &user_data.application_deployments {
// Check if this deployment belongs to an app from our app provider
// We need to get the application provider's apps to match
let provider_apps = UserPersistence::get_user_apps(application_provider_email);
for provider_app in &provider_apps {
if deployment.app_id == provider_app.id && deployment.status == "Active" {
*deployment_counts.entry(provider_app.id.clone()).or_insert(0) += 1;
}
}
}
}
}
}
}
}
}
deployment_counts
}
/// Renders the dashboard home page (shows login prompt if not authenticated)
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("dashboard")
.active_section("home")
.gitea_enabled(get_app_config().is_gitea_enabled())
.build();
// Inject currency display variables
{
let currency_service = match crate::services::currency::CurrencyService::builder().build() {
Ok(svc) => svc,
Err(_) => crate::services::currency::CurrencyService::new(),
};
let display_currency = currency_service.get_user_preferred_currency(&session);
let currency_symbol = currency_service
.get_currency(&display_currency)
.map(|c| c.symbol)
.unwrap_or_else(|| "$".to_string());
ctx.insert("display_currency", &display_currency);
ctx.insert("currency_symbol", &currency_symbol);
}
// Check if user is logged in and load with persistent data
if let Some(user) = Self::load_user_with_persistent_data(&session) {
// User is logged in
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
}
// Load persistent dashboard data for templates
if let Ok(Some(user_email)) = session.get::<String>("user_email") {
ctx.insert("user_email", &user_email);
if let Ok(user_service) = UserService::builder().build() {
let user_metrics = user_service.calculate_user_metrics(&user_email);
ctx.insert("user_metrics", &user_metrics);
let compute_resources = user_service.get_user_compute_resources(&user_email);
let active_compute_resources_count = compute_resources.len();
ctx.insert("compute_resources", &compute_resources);
ctx.insert("active_compute_resources_count", &active_compute_resources_count);
let recent_activities = user_service.get_user_activities(&user_email, Some(10));
// Map activities to template-friendly fields expected by dashboard/index.html
let recent_activities_table: Vec<serde_json::Value> = recent_activities
.iter()
.map(|a| {
let action = match a.activity_type {
crate::models::user::ActivityType::Login => "Login",
crate::models::user::ActivityType::Logout => "Logout",
crate::models::user::ActivityType::PasswordChange => "Password Change",
crate::models::user::ActivityType::CreditsTopup => "Credits Top-up",
crate::models::user::ActivityType::NodeDeployment => "Node Deployment",
crate::models::user::ActivityType::AppDeployment => "App Deployment",
crate::models::user::ActivityType::SliceRental => "Slice Rental",
crate::models::user::ActivityType::Purchase => "Purchase",
crate::models::user::ActivityType::Deployment => "Deployment",
crate::models::user::ActivityType::ServiceCreated => "Service Created",
crate::models::user::ActivityType::AppPublished => "App Published",
crate::models::user::ActivityType::NodeAdded => "Node Added",
crate::models::user::ActivityType::NodeUpdated => "Node Updated",
crate::models::user::ActivityType::ServiceRequested => "Service Requested",
crate::models::user::ActivityType::WalletTopup => "Wallet Top-up",
crate::models::user::ActivityType::ServiceCompleted => "Service Completed",
crate::models::user::ActivityType::WalletTransaction => "Wallet Transaction",
crate::models::user::ActivityType::ProfileUpdate => "Profile Update",
crate::models::user::ActivityType::SettingsChange => "Settings Change",
crate::models::user::ActivityType::MarketplaceView => "Marketplace View",
crate::models::user::ActivityType::SliceCreated => "Slice Created",
crate::models::user::ActivityType::SliceAllocated => "Slice Allocated",
crate::models::user::ActivityType::SliceReleased => "Slice Released",
crate::models::user::ActivityType::SliceRentalStarted => "Slice Rental Started",
crate::models::user::ActivityType::SliceRentalStopped => "Slice Rental Stopped",
crate::models::user::ActivityType::SliceRentalRestarted => "Slice Rental Restarted",
crate::models::user::ActivityType::SliceRentalCancelled => "Slice Rental Cancelled",
};
let status: String = a
.metadata
.as_ref()
.and_then(|meta| meta.get("status"))
.and_then(|v| v.as_str())
.unwrap_or_else(|| match a.activity_type {
crate::models::user::ActivityType::Deployment => "Active",
_ => "Completed",
})
.to_string();
serde_json::json!({
"date": a.timestamp.format("%Y-%m-%d %H:%M").to_string(),
"action": action,
"status": status,
"details": a.description
})
})
.collect();
ctx.insert("recent_activities", &recent_activities_table);
// Sum monthly cost across active compute resources
let total_monthly_cost: rust_decimal::Decimal = compute_resources
.iter()
.map(|r| r.monthly_cost)
.sum();
ctx.insert("total_monthly_cost", &total_monthly_cost);
}
}
ctx.insert("user", &user);
// Render the dashboard home page for authenticated users
match render_template(&tmpl, "dashboard/index.html", &ctx) {
Ok(response) => {
Ok(response)
},
Err(_e) => {
ResponseBuilder::internal_error()
.body("Template rendering failed").build()
}
}
} else {
// User is not logged in, render the welcome page (this is the nice UX page)
render_template(&tmpl, "dashboard/welcome.html", &ctx)
}
}
/// Renders the user section of the dashboard
pub async fn user_section(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("dashboard")
.active_section("user")
.gitea_enabled(get_app_config().is_gitea_enabled())
.build();
// Inject currency display variables
{
let currency_service = match crate::services::currency::CurrencyService::builder().build() {
Ok(svc) => svc,
Err(_) => crate::services::currency::CurrencyService::new(),
};
let display_currency = currency_service.get_user_preferred_currency(&session);
let currency_symbol = currency_service
.get_currency(&display_currency)
.map(|c| c.symbol)
.unwrap_or_else(|| "$".to_string());
ctx.insert("display_currency", &display_currency);
ctx.insert("currency_symbol", &currency_symbol);
}
// Check if user is logged in
if session.get::<String>("user").unwrap_or(None).is_none() {
// User is not logged in, show welcome page with login/register options
return render_template(&tmpl, "dashboard/welcome.html", &ctx);
}
// Load user with real data (no mock data)
if let Ok(Some(user_email)) = session.get::<String>("user_email") {
ctx.insert("user_email", &user_email);
// Build user service for comprehensive data loading
if let Ok(user_service) = crate::services::user_service::UserService::builder().build() {
// Load user applications (purchased/deployed apps)
let user_applications = user_service.get_user_applications(&user_email);
ctx.insert("user_applications", &user_applications);
let active_applications_count = user_applications.len();
ctx.insert("active_applications_count", &active_applications_count);
// Load user services (purchased services)
let user_services = user_service.get_user_purchased_services(&user_email);
ctx.insert("user_services", &user_services);
// Load user compute resources (slice rentals)
let compute_resources = user_service.get_user_compute_resources(&user_email);
let active_compute_resources_count = compute_resources.len();
ctx.insert("compute_resources", &compute_resources);
ctx.insert("active_compute_resources_count", &active_compute_resources_count);
let total_monthly_cost: rust_decimal::Decimal = compute_resources.iter().map(|r| r.monthly_cost).sum();
ctx.insert("total_monthly_cost", &total_monthly_cost);
// Calculate average SLA percentage across compute resources (fallback to 99.0 if unavailable)
let mut sla_sum: f64 = 0.0;
let mut sla_count: u32 = 0;
for res in &compute_resources {
let s = res.sla.trim();
if !s.is_empty() && s != "Unknown" {
// Keep only digits and decimal point (handles values like "99.9%" or "SLA 99.9%")
let cleaned: String = s.chars().filter(|c| c.is_ascii_digit() || *c == '.').collect();
if let Ok(v) = cleaned.parse::<f64>() {
if v > 0.0 && v <= 100.0 {
sla_sum += v;
sla_count += 1;
}
}
}
}
let average_sla_percentage: f64 = if sla_count > 0 { sla_sum / sla_count as f64 } else { 99.0 };
ctx.insert("average_sla_percentage", &average_sla_percentage);
// Load user activities
let recent_activities = user_service.get_user_activities(&user_email, Some(10));
ctx.insert("recent_activities", &recent_activities);
// Calculate user metrics
let user_metrics = user_service.calculate_user_metrics(&user_email);
ctx.insert("user_metrics", &user_metrics);
}
// Load user data from session (without mock data override)
if let Ok(Some(user_json)) = session.get::<String>("user") {
if let Ok(mut user) = serde_json::from_str::<crate::models::user::User>(&user_json) {
// Apply session data if available
if let Some(session_data) = SessionManager::load_user_session_data(&session) {
SessionManager::apply_session_to_user(&mut user, &session_data);
}
// Apply persistent data (profile info) but keep user data separate
if let Some(persistent_data) = UserPersistence::load_user_data(&user_email) {
if let Some(name) = persistent_data.name {
user.name = name;
}
if let Some(country) = persistent_data.country {
user.country = Some(country);
}
if let Some(timezone) = persistent_data.timezone {
user.timezone = Some(timezone);
}
// Add slice rental data
ctx.insert("slice_rentals", &persistent_data.slice_rentals);
ctx.insert("wallet_balance", &persistent_data.wallet_balance_usd);
}
ctx.insert("user_json", &user_json);
ctx.insert("user", &user);
}
}
// Load slice rental service to get additional statistics
if let Ok(slice_rental_service) = crate::services::slice_rental::SliceRentalService::builder().build() {
let user_slice_rentals = slice_rental_service.get_user_slice_rentals(&user_email);
ctx.insert("active_slice_rentals", &user_slice_rentals);
// Calculate slice rental statistics with deployment type breakdown
let total_active_rentals = user_slice_rentals.len();
let total_monthly_cost: rust_decimal::Decimal = user_slice_rentals.iter()
.map(|rental| rental.slice_allocation.monthly_cost)
.sum();
// Count deployment types
let mut vm_count = 0;
let k8s_count = 0;
for _rental in &user_slice_rentals {
// For now, we'll assume all rentals are VM deployments
// In a real implementation, this would be stored in the rental metadata
vm_count += 1;
}
ctx.insert("slice_rental_stats", &serde_json::json!({
"total_active_rentals": total_active_rentals,
"total_monthly_cost": total_monthly_cost,
"vm_deployments": vm_count,
"kubernetes_deployments": k8s_count
}));
}
}
render_template(&tmpl, "dashboard/user.html", &ctx)
}
/// Renders the resource provider section of the dashboard
pub async fn resource_provider_section(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("dashboard")
.active_section("resource_provider")
.gitea_enabled(get_app_config().is_gitea_enabled())
.build();
// Check if user is logged in
if session.get::<String>("user").unwrap_or(None).is_none() {
// User is not logged in, show welcome page with login/register options
return render_template(&tmpl, "dashboard/welcome.html", &ctx);
}
// RESOURCE PROVIDER FIX: Use persistent data only, no mock data for resource provider dashboard
if let Ok(Some(user_email)) = session.get::<String>("user_email") {
ctx.insert("user_email", &user_email);
// Initialize resource provider service with slice calculator
if let Ok(resource_provider_service) = crate::services::resource_provider::ResourceProviderService::builder().build() {
// Repair node-group data consistency when resource provider dashboard loads
if let Err(_e) = resource_provider_service.repair_node_group_consistency(&user_email) {
}
// Repair missing marketplace SLA data for existing nodes
if let Err(_e) = resource_provider_service.repair_missing_marketplace_sla(
&user_email,
99.8, // default uptime
100, // default bandwidth
rust_decimal::Decimal::from_str("0.5").unwrap_or_default() // default price
) {
}
// Get resource provider nodes with updated slice calculations
let resource_provider_nodes = resource_provider_service.get_resource_provider_nodes(&user_email);
// Calculate resource_provider statistics from nodes
let total_nodes = resource_provider_nodes.len() as u32;
let mut online_nodes = 0u32;
let mut total_base_slices = 0u32;
let mut allocated_base_slices = 0u32;
let mut total_monthly_earnings = rust_decimal::Decimal::ZERO;
let mut average_uptime = 0.0f32;
for node in &resource_provider_nodes {
if matches!(node.status, crate::models::user::NodeStatus::Online) {
online_nodes += 1;
}
total_base_slices += node.total_base_slices as u32;
allocated_base_slices += node.allocated_base_slices as u32;
total_monthly_earnings += node.earnings_today_usd * rust_decimal::Decimal::from(30); // Estimate monthly
average_uptime += node.uptime_percentage;
}
if total_nodes > 0 {
average_uptime /= total_nodes as f32;
}
// Create resource_provider statistics for the dashboard
let resource_provider_stats = serde_json::json!({
"total_nodes": total_nodes,
"online_nodes": online_nodes,
"total_base_slices": total_base_slices,
"allocated_base_slices": allocated_base_slices,
"available_base_slices": total_base_slices - allocated_base_slices,
"average_uptime": average_uptime,
"monthly_earnings": total_monthly_earnings,
"slice_utilization_percentage": if total_base_slices > 0 {
(allocated_base_slices as f32 / total_base_slices as f32) * 100.0
} else {
0.0
}
});
ctx.insert("resource_provider_stats", &resource_provider_stats);
ctx.insert("resource_provider_nodes", &resource_provider_nodes);
}
// Load user data from session (without mock data override)
if let Ok(Some(user_json)) = session.get::<String>("user") {
if let Ok(mut user) = serde_json::from_str::<crate::models::user::User>(&user_json) {
// Apply session data if available
if let Some(session_data) = SessionManager::load_user_session_data(&session) {
SessionManager::apply_session_to_user(&mut user, &session_data);
}
// Apply persistent data (profile info)
if let Some(persistent_data) = UserPersistence::load_user_data(&user_email) {
if let Some(name) = persistent_data.name {
user.name = name;
}
if let Some(country) = persistent_data.country {
user.country = Some(country);
}
if let Some(timezone) = persistent_data.timezone {
user.timezone = Some(timezone);
}
ctx.insert("wallet_balance", &persistent_data.wallet_balance_usd);
ctx.insert("resource_provider_earnings", &persistent_data.resource_provider_earnings);
}
ctx.insert("user_json", &user_json);
ctx.insert("user", &user);
}
}
// Load slice rental service to get resource_provider slice rental statistics
if let Ok(slice_rental_service) = crate::services::slice_rental::SliceRentalService::builder().build() {
let resource_provider_slice_stats = slice_rental_service.get_resource_provider_slice_statistics(&user_email);
ctx.insert("slice_rental_statistics", &resource_provider_slice_stats);
// Release any expired rentals
if let Err(_e) = slice_rental_service.release_expired_rentals(&user_email) {
}
}
}
render_template(&tmpl, "dashboard/resource_provider.html", &ctx)
}
/// Renders the application provider section of the dashboard
pub async fn application_provider_section(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("dashboard")
.build();
ctx.insert("active_section", "application_provider");
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Add user to context if available
if let Some(user) = Self::load_user_with_persistent_data(&session) {
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
}
ctx.insert("user", &user);
// Add user_email for messaging system
if let Ok(Some(user_email)) = session.get::<String>("user_email") {
ctx.insert("user_email", &user_email);
}
// Build persistent app provider data for hydration and summary cards
let email = user.email.clone();
// Load fresh persistent apps and adjust deployment counts across all users
let mut fresh_apps = crate::services::user_persistence::UserPersistence::get_user_apps(&email);
let cross_user_deployment_counts = Self::count_cross_user_deployments(&email);
for app in &mut fresh_apps {
if let Some(&count) = cross_user_deployment_counts.get(&app.id) {
app.deployments = count;
}
}
// Load fresh persistent deployments
let fresh_deployments = crate::services::user_persistence::UserPersistence::get_user_application_deployments(&email);
// Only count deployments for apps published by this user
let user_published_app_ids: std::collections::HashSet<String> = fresh_apps.iter().map(|a| a.id.clone()).collect();
let active_deployments = fresh_deployments
.iter()
.filter(|d| d.status == "Active" && user_published_app_ids.contains(&d.app_id))
.count() as i32;
let monthly_revenue_usd = fresh_apps.iter().map(|a| {
use std::str::FromStr;
let decimal_val: rust_decimal::Decimal = a.monthly_revenue_usd;
i32::from_str(&decimal_val.to_string()).unwrap_or(0)
}).sum::<i32>();
let total_revenue_usd = monthly_revenue_usd * 12; // Simple estimate
// Build deployment stats enriched with auto_healing
let deployment_stats: Vec<crate::models::user::DeploymentStat> = fresh_deployments
.iter()
.filter(|d| user_published_app_ids.contains(&d.app_id))
.map(|d| {
let auto_healing = d.auto_healing.unwrap_or_else(|| {
fresh_apps
.iter()
.find(|app| app.id == d.app_id)
.and_then(|app| app.auto_healing)
.unwrap_or(false)
});
crate::models::user::DeploymentStat {
app_name: d.app_name.clone(),
region: d.region.clone(),
active_instances: d.instances,
total_instances: d.instances,
avg_response_time_ms: Some(100.0), // Default response time
uptime_percentage: Some(99.5), // Default uptime
status: d.status.clone(),
instances: d.instances,
resource_usage: Some(serde_json::to_string(&d.resource_usage).unwrap_or_default()),
customer_name: Some(d.customer_name.clone()),
deployed_date: {
use chrono::DateTime;
DateTime::parse_from_rfc3339(&d.deployed_date)
.or_else(|_| DateTime::parse_from_str(&d.deployed_date, "%Y-%m-%d %H:%M:%S"))
.map(|dt| dt.with_timezone(&chrono::Utc))
.ok()
},
deployment_id: Some(d.id.clone()),
last_deployment: {
use chrono::DateTime;
DateTime::parse_from_rfc3339(&d.deployed_date)
.or_else(|_| DateTime::parse_from_str(&d.deployed_date, "%Y-%m-%d %H:%M:%S"))
.map(|dt| dt.with_timezone(&chrono::Utc))
.ok()
},
auto_healing: Some(auto_healing),
}
})
.collect();
let application_provider_data = crate::models::user::AppProviderData {
published_apps: fresh_apps.len() as i32,
total_deployments: fresh_apps.iter().map(|a| a.deployments).sum::<i32>(),
active_deployments,
monthly_revenue_usd,
total_revenue_usd,
apps: fresh_apps,
deployment_stats,
revenue_history: Vec::new(),
};
ctx.insert("application_provider_data", &application_provider_data);
} else {
// Ensure template always has a defined structure even without a logged-in user
let empty: crate::models::user::AppProviderData = crate::models::user::AppProviderData {
published_apps: 0,
total_deployments: 0,
active_deployments: 0,
monthly_revenue_usd: 0,
total_revenue_usd: 0,
apps: Vec::new(),
deployment_stats: Vec::new(),
revenue_history: Vec::new(),
};
ctx.insert("application_provider_data", &empty);
}
render_template(&tmpl, "dashboard/application_provider.html", &ctx)
}
/// Renders the service provider section of the dashboard
pub async fn service_provider_section(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("dashboard")
.build();
ctx.insert("active_section", "service_provider");
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Add user to context if available
if let Some(user) = Self::load_user_with_persistent_data(&session) {
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
}
ctx.insert("user", &user);
// Add user_email for messaging system
if let Ok(Some(user_email)) = session.get::<String>("user_email") {
ctx.insert("user_email", &user_email);
}
// Build persistent service provider data for hydration and summary cards
let email = user.email.clone();
let fresh_services = crate::services::user_persistence::UserPersistence::get_user_services(&email);
let fresh_requests = crate::services::user_persistence::UserPersistence::get_user_service_requests(&email);
let service_provider_data = crate::models::user::ServiceProviderData {
active_services: fresh_services.len() as i32,
total_clients: fresh_services.iter().map(|s| s.clients).sum::<i32>(),
monthly_revenue_usd: fresh_services
.iter()
.map(|s| s.price_per_hour_usd * s.total_hours.unwrap_or(0))
.sum::<i32>(),
total_revenue_usd: fresh_services
.iter()
.map(|s| s.price_per_hour_usd * s.total_hours.unwrap_or(0) * 2)
.sum::<i32>(), // Simple estimate
service_rating: if fresh_services.is_empty() {
0.0
} else {
fresh_services.iter().map(|s| s.rating).sum::<f32>() / fresh_services.len() as f32
},
services: fresh_services,
client_requests: fresh_requests,
availability: Some(true), // Default to available
hourly_rate_range: Some("$50-$150".to_string()), // Default range
last_payment_method: Some("Credit Card".to_string()), // Default payment method
revenue_history: Vec::new(),
};
ctx.insert("service_provider_data", &service_provider_data);
} else {
// Ensure template always has a defined structure even without a logged-in user
let empty = serde_json::json!({
"active_services": 0,
"total_clients": 0,
"monthly_revenue_usd": 0,
"total_revenue_usd": 0,
"service_rating": 0.0,
"services": [],
"client_requests": [],
"revenue_history": []
});
ctx.insert("service_provider_data", &empty);
}
render_template(&tmpl, "dashboard/service_provider.html", &ctx)
}
/// Renders the settings page
pub async fn settings(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("dashboard")
.build();
ctx.insert("active_section", "settings");
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Check if user is logged in
if session.get::<String>("user").unwrap_or(None).is_none() {
// User is not logged in, show welcome page with login/register options
return render_template(&tmpl, "dashboard/welcome.html", &ctx);
}
// Add user to context if available
if let Some(user) = Self::load_user_with_persistent_data(&session) {
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
}
ctx.insert("user", &user);
// Load user's currency preferences and SSH keys from persistent data
if let Ok(Some(user_email)) = session.get::<String>("user_email") {
ctx.insert("user_email", &user_email);
if let Some(user_data) = crate::services::user_persistence::UserPersistence::load_user_data(&user_email) {
ctx.insert("user_display_currency", &user_data.display_currency.unwrap_or_else(|| "USD".to_string()));
ctx.insert("user_quick_topup_amounts", &user_data.quick_topup_amounts.unwrap_or_else(|| vec![
rust_decimal_macros::dec!(10),
rust_decimal_macros::dec!(25),
rust_decimal_macros::dec!(50),
rust_decimal_macros::dec!(100)
]));
} else {
// Default values if no persistent data found
ctx.insert("user_display_currency", &"USD".to_string());
ctx.insert("user_quick_topup_amounts", &vec![
rust_decimal_macros::dec!(10),
rust_decimal_macros::dec!(25),
rust_decimal_macros::dec!(50),
rust_decimal_macros::dec!(100)
]);
}
// Load SSH keys for the settings page
if let Ok(ssh_service) = crate::services::ssh_key_service::SSHKeyService::builder().build() {
let req_id = uuid::Uuid::new_v4().to_string();
let ssh_keys = ssh_service.get_user_ssh_keys_async(&user_email, Some(&req_id)).await;
ctx.insert("ssh_keys", &ssh_keys);
// Convert to JSON string for CSP-compliant hydration
match serde_json::to_string(&ssh_keys) {
Ok(ssh_keys_json) => {
ctx.insert("ssh_keys_json", &ssh_keys_json);
}
Err(e) => {
log::error!("Failed to serialize SSH keys for user {}: {}", user_email, e);
ctx.insert("ssh_keys_json", "[]");
}
}
} else {
log::error!("Failed to build SSH key service for settings page");
ctx.insert("ssh_keys", &Vec::<crate::models::ssh_key::SSHKey>::new());
ctx.insert("ssh_keys_json", "[]");
}
}
}
render_template(&tmpl, "dashboard/settings.html", &ctx)
}
/// Renders the messages page
pub async fn messages_page(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("dashboard")
.build();
ctx.insert("active_section", "messages");
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Check if user is logged in
if session.get::<String>("user").unwrap_or(None).is_none() {
// User is not logged in, show welcome page with login/register options
return render_template(&tmpl, "dashboard/welcome.html", &ctx);
}
// Add user to context if available
if let Some(user) = Self::load_user_with_persistent_data(&session) {
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
}
ctx.insert("user", &user);
// Add user email for messaging system
if let Ok(Some(user_email)) = session.get::<String>("user_email") {
ctx.insert("user_email", &user_email);
}
}
render_template(&tmpl, "dashboard/messages.html", &ctx)
}
/// Renders the MC Credits pools page
pub async fn pools(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("dashboard")
.build();
ctx.insert("active_section", "pools");
let config = get_app_config();
ctx.insert("gitea_enabled", &config.is_gitea_enabled());
// Add user to context if available
if let Some(user) = Self::load_user_with_persistent_data(&session) {
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
}
ctx.insert("user", &user);
// Add user_email for messaging system
if let Ok(Some(user_email)) = session.get::<String>("user_email") {
ctx.insert("user_email", &user_email);
}
}
render_template(&tmpl, "dashboard/pools.html", &ctx)
}
/// API endpoint to return resource_provider dashboard data as JSON
pub async fn resource_provider_data_api(session: Session) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email").unwrap_or_default().unwrap_or_default();
// RESOURCE_PROVIDER FIX: Use resource_provider service to ensure data consistency
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(_e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
// Repair data consistency before loading
if let Err(_e) = resource_provider_service.repair_node_group_consistency(&user_email) {
}
// Load real resource_provider data from persistence using resource_provider service
let nodes = resource_provider_service.get_resource_provider_nodes(&user_email);
let earnings = resource_provider_service.get_resource_provider_earnings(&user_email);
let stats = resource_provider_service.get_resource_provider_statistics(&user_email);
// Always use persistent data - no fallback to mock data for resource_provider dashboard
// If no data exists, return empty but valid resource_provider data structure
if nodes.is_empty() {
return Ok(ResponseBuilder::ok()
.json(serde_json::json!({
"total_nodes": 0,
"online_nodes": 0,
"total_capacity": {
"cpu_cores": 0,
"memory_gb": 0,
"storage_gb": 0,
"network_gb": 0
},
"monthly_earnings": {
"tft": 0,
"usd": 0
},
"nodes": [],
"earnings_history": [],
"last_updated": chrono::Utc::now().to_rfc3339()
}))
.build());
}
// Load slice products for this resource_provider
let slice_products = crate::services::user_persistence::UserPersistence::get_slice_products(&user_email);
let active_slices = slice_products.len() as i32;
// Build comprehensive resource_provider data using statistics from resource_provider service
let resource_provider_data = serde_json::json!({
"total_nodes": stats.total_nodes,
"online_nodes": stats.online_nodes,
"total_capacity": stats.total_capacity,
"used_capacity": stats.used_capacity,
"monthly_earnings": stats.monthly_earnings,
"total_earnings": stats.monthly_earnings * 3, // Estimate
"uptime_percentage": stats.uptime_percentage,
"nodes": nodes,
"earnings_history": earnings,
"active_slices": active_slices,
"slice_products": slice_products
});
Ok(ResponseBuilder::ok()
.json(resource_provider_data)
.build())
}
/// Enhanced resource_provider dashboard with node management
pub async fn resource_provider_dashboard_enhanced(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let resource_provider_service = crate::services::resource_provider::ResourceProviderService::builder()
.auto_sync_enabled(true)
.metrics_collection(true)
.build()
.map_err(|_e| {
actix_web::error::ErrorInternalServerError("Service initialization failed")
})?;
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
// Load resource_provider data using the service
let nodes = resource_provider_service.get_resource_provider_nodes(&user_email);
let earnings = resource_provider_service.get_resource_provider_earnings(&user_email);
let stats = resource_provider_service.get_resource_provider_statistics(&user_email);
let mut ctx = crate::models::builders::ContextBuilder::new()
.active_page("dashboard")
.active_section("resource_provider")
.build();
ctx.insert("nodes", &nodes);
ctx.insert("earnings", &earnings);
ctx.insert("stats", &stats);
// Add user to context if available
if let Some(user) = Self::load_user_with_persistent_data(&session) {
if let Ok(Some(user_json)) = session.get::<String>("user") {
ctx.insert("user_json", &user_json);
}
ctx.insert("user", &user);
}
render_template(&tmpl, "dashboard/resource_provider.html", &ctx)
}
/// API endpoint to add a new farm node using ResourceProviderService
pub async fn add_farm_node_enhanced(session: Session, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
// Initialize resource_provider service
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(_e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
// Extract node data from form using builder pattern
let node_data_json = form.into_inner();
// Extract slice formats array
let slice_formats = node_data_json.get("slice_formats")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect::<Vec<String>>()
});
// Extract rental configuration
let slice_rental_enabled = node_data_json.get("slice_rental_enabled")
.and_then(|v| v.as_bool())
.unwrap_or(true); // Always true by default
let full_node_rental_enabled = node_data_json.get("full_node_rental_enabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let full_node_price = if full_node_rental_enabled {
node_data_json.get("full_node_price")
.and_then(|v| v.as_f64())
.map(|p| Decimal::from_f64_retain(p).unwrap_or(Decimal::ZERO))
} else {
None
};
let minimum_rental_days = node_data_json.get("minimum_rental_days")
.and_then(|v| v.as_u64())
.unwrap_or(30) as u32;
// Build rental options using builder pattern
let rental_options = if slice_rental_enabled || full_node_rental_enabled {
let mut builder = crate::models::builders::NodeRentalOptionsBuilder::new()
.slice_rental_enabled(slice_rental_enabled)
.full_node_rental_enabled(full_node_rental_enabled)
.minimum_rental_days(minimum_rental_days)
.auto_renewal_enabled(false);
// Add legacy monthly price as basic pricing if provided
if let Some(price) = full_node_price {
if let Ok(pricing) = crate::models::builders::FullNodePricingBuilder::new()
.monthly(price)
.auto_calculate(false)
.build() {
builder = builder.full_node_pricing(pricing);
}
}
builder.build().ok()
} else {
None
};
// Extract slice prices
let slice_prices = node_data_json.get("slice_prices")
.and_then(|v| v.as_object())
.map(|obj| {
let mut prices = std::collections::HashMap::new();
if let Some(basic) = obj.get("basic").and_then(|v| v.as_f64()) {
prices.insert("basic".to_string(), Decimal::from_f64_retain(basic).unwrap_or(Decimal::from(25)));
}
if let Some(standard) = obj.get("standard").and_then(|v| v.as_f64()) {
prices.insert("standard".to_string(), Decimal::from_f64_retain(standard).unwrap_or(Decimal::from(50)));
}
if let Some(performance) = obj.get("performance").and_then(|v| v.as_f64()) {
prices.insert("performance".to_string(), Decimal::from_f64_retain(performance).unwrap_or(Decimal::from(100)));
}
prices
});
// Build NodeCreationData using builder pattern
let mut node_data_builder = crate::models::builders::NodeCreationDataBuilder::new()
.name(node_data_json.get("name")
.and_then(|v| v.as_str())
.unwrap_or("New Node"))
.location(node_data_json.get("location")
.and_then(|v| v.as_str())
.unwrap_or("Unknown"))
.cpu_cores(node_data_json.get("cpu_cores")
.and_then(|v| v.as_i64())
.unwrap_or(4) as i32)
.memory_gb(node_data_json.get("memory_gb")
.and_then(|v| v.as_i64())
.unwrap_or(8) as i32)
.storage_gb(node_data_json.get("storage_gb")
.and_then(|v| v.as_i64())
.unwrap_or(100) as i32)
.bandwidth_mbps(node_data_json.get("bandwidth_mbps")
.and_then(|v| v.as_i64())
.unwrap_or(100) as i32)
.node_type("MyceliumNode"); // Always MyceliumNode - resource providers register MyceliumNodes to Mycelium Grid
// Add optional fields
if let Some(region) = node_data_json.get("region").and_then(|v| v.as_str()) {
node_data_builder = node_data_builder.region(region);
}
if let Some(formats) = slice_formats {
node_data_builder = node_data_builder.slice_formats(formats);
}
if let Some(options) = rental_options {
node_data_builder = node_data_builder.rental_options(options);
}
if let Some(prices) = slice_prices {
node_data_builder = node_data_builder.slice_prices(prices);
}
let node_data = node_data_builder.build().map_err(|e| {
actix_web::error::ErrorBadRequest(format!("Invalid node data: {}", e))
})?;
// Add node using resource_provider service
match resource_provider_service.add_node(&user_email, node_data) {
Ok(node) => {
// Add activity record
let activity = crate::models::builders::UserActivityBuilder::new()
.activity_type(crate::models::user::ActivityType::NodeAdded)
.description(format!("Added new farm node: {}", node.name))
.category("Farming".to_string())
.importance(crate::models::user::ActivityImportance::High)
.build()
.unwrap();
let _ = UserPersistence::add_user_activity(&user_email, activity);
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Farm node added successfully",
"node": node
})).build())
},
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to add farm node",
"details": e
})).build())
}
}
}
/// API endpoint to update node status
pub async fn update_node_status(session: Session, path: web::Path<String>, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
let node_id = path.into_inner();
let status_str = form.get("status")
.and_then(|v| v.as_str())
.unwrap_or("Offline");
let status = match status_str {
"Online" => crate::models::user::NodeStatus::Online,
"Offline" => crate::models::user::NodeStatus::Offline,
"Maintenance" => crate::models::user::NodeStatus::Maintenance,
"Error" => crate::models::user::NodeStatus::Offline,
"Standby" => crate::models::user::NodeStatus::Standby,
_ => crate::models::user::NodeStatus::Offline,
};
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
match resource_provider_service.update_node_status(&user_email, &node_id, status) {
Ok(()) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Node status updated successfully"
})).build())
},
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to update node status",
"details": e
})).build())
}
}
}
/// API endpoint to get node details by ID
pub async fn get_node_details(session: Session, path: web::Path<String>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
let node_id = path.into_inner();
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
match resource_provider_service.get_node_by_id(&user_email, &node_id) {
Some(mut node) => {
// MARKETPLACE SLA FIX: Override grid data with marketplace SLA values when available
if let Some(ref sla) = node.marketplace_sla {
// Override capacity bandwidth with SLA value
// Use uptime_guarantee as a proxy for bandwidth (temporary fix)
node.capacity.bandwidth_mbps = (sla.uptime_guarantee * 100.0) as i32;
// Override uptime with SLA value
node.uptime_percentage = sla.uptime_guarantee_percentage;
// Override slice pricing with SLA value
if let Some(ref mut pricing) = node.slice_pricing {
if let Some(pricing_obj) = pricing.as_object_mut() {
pricing_obj.insert("base_price_per_hour".to_string(), serde_json::Value::from(sla.base_slice_price.to_f64().unwrap_or(0.0)));
}
}
} else {
}
Ok(ResponseBuilder::ok().json(node).build())
},
None => {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"error": "Node not found"
})).build())
}
}
}
/// API endpoint to get slice statistics
pub async fn get_slice_statistics(session: Session) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
let stats = resource_provider_service.get_resource_provider_statistics(&user_email);
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"statistics": {
"total_base_slices": stats.total_base_slices,
"allocated_base_slices": stats.allocated_base_slices,
"available_base_slices": stats.total_base_slices - stats.allocated_base_slices,
"slice_utilization_percentage": if stats.total_base_slices > 0 {
(stats.allocated_base_slices as f32 / stats.total_base_slices as f32) * 100.0
} else {
0.0
}
}
})).build())
}
/// API endpoint to update node configuration
pub async fn update_node_comprehensive(session: Session, path: web::Path<String>, form: web::Json<crate::services::resource_provider::NodeUpdateData>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
let node_id = path.into_inner();
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
match resource_provider_service.update_node(&user_email, &node_id, form.into_inner()) {
Ok(()) => {
// Add activity record
let activity = crate::models::builders::UserActivityBuilder::new()
.activity_type(crate::models::user::ActivityType::NodeUpdated)
.description(format!("Updated node configuration: {}", node_id))
.category("Farming".to_string())
.importance(crate::models::user::ActivityImportance::Medium)
.build()
.unwrap();
let _ = UserPersistence::add_user_activity(&user_email, activity);
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Node updated successfully"
})).build())
},
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to update node",
"details": e
})).build())
}
}
}
/// API endpoint to get default slice formats
pub async fn get_default_slice_formats(session: Session) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
// Get default formats with user customizations applied
let default_format_ids = vec!["basic", "standard", "performance"];
let mut customized_formats = Vec::new();
for format_id in default_format_ids {
if let Some(format) = resource_provider_service.get_default_slice_format_with_customizations(&user_email, format_id) {
customized_formats.push(format);
}
}
Ok(ResponseBuilder::ok().json(customized_formats).build())
}
/// API endpoint to get node groups
pub async fn get_node_groups(session: Session) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
let groups = resource_provider_service.get_node_groups(&user_email);
Ok(ResponseBuilder::ok().json(groups).build())
}
/// API endpoint to create custom node group
pub async fn create_custom_node_group(session: Session, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
let form_data = form.into_inner();
let name = form_data.get("name")
.and_then(|v| v.as_str())
.unwrap_or("Custom Group")
.to_string();
let description = form_data.get("description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
match resource_provider_service.create_custom_node_group(&user_email, name, description, None) {
Ok(group) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Custom node group created successfully",
"group": group
})).build())
},
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to create custom node group",
"details": e
})).build())
}
}
}
/// API endpoint to assign node to group
pub async fn assign_node_to_group(session: Session, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
let form_data = form.into_inner();
let node_id = form_data.get("node_id")
.and_then(|v| v.as_str())
.ok_or_else(|| actix_web::error::ErrorBadRequest("Node ID is required"))?;
let group_id = form_data.get("group_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
match resource_provider_service.update_node_group_assignment(&user_email, node_id, group_id) {
Ok(group_name) => {
// RESOURCE_PROVIDER FIX: Repair consistency after group assignment change
if let Err(e) = resource_provider_service.repair_node_group_consistency(&user_email) {
}
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Node assigned to group successfully",
"group_name": group_name,
"refresh_required": true // Signal frontend to refresh node groups
})).build())
},
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to assign node to group",
"details": e
})).build())
}
}
}
/// API endpoint to delete custom node group
pub async fn delete_custom_node_group(session: Session, path: web::Path<String>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
let group_id = path.into_inner();
match resource_provider_service.delete_custom_node_group(&user_email, &group_id) {
Ok(()) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Custom node group deleted successfully"
})).build())
},
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to delete custom node group",
"details": e
})).build())
}
}
}
/// Get details for a specific default slice format
pub async fn get_default_slice_details(session: Session, path: web::Path<String>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
let format_id = path.into_inner();
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
match resource_provider_service.get_default_slice_format_with_customizations(&user_email, &format_id) {
Some(format) => {
Ok(ResponseBuilder::ok().json(format).build())
}
None => {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"error": "Default slice format not found"
})).build())
}
}
}
/// Save default slice customizations
pub async fn save_default_slice_customization(session: Session, path: web::Path<String>, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
let format_id = path.into_inner();
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Service initialization failed"
})).build());
}
};
// Parse the customization data
let name = form.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
let description = form.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
let cpu_cores = form.get("cpu_cores").and_then(|v| v.as_i64()).unwrap_or(2) as i32;
let memory_gb = form.get("memory_gb").and_then(|v| v.as_i64()).unwrap_or(4) as i32;
let storage_gb = form.get("storage_gb").and_then(|v| v.as_i64()).unwrap_or(100) as i32;
let bandwidth_mbps = form.get("bandwidth_mbps").and_then(|v| v.as_i64()).unwrap_or(100) as i32;
let price_per_hour = form.get("price_per_hour").and_then(|v| v.as_f64()).map(rust_decimal::Decimal::from_f64_retain).flatten().unwrap_or(rust_decimal::Decimal::from(10));
let customization = crate::services::resource_provider::DefaultSliceFormat {
id: format_id.clone(),
name,
cpu_cores,
memory_gb,
storage_gb,
bandwidth_mbps,
description,
price_per_hour,
};
match resource_provider_service.save_default_slice_customization(&user_email, &format_id, customization) {
Ok(_) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Default slice customization saved successfully"
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to save customization",
"details": e
})).build())
}
}
}
/// Get slice products for the authenticated user
pub async fn get_slice_products(session: Session) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email").unwrap_or_default().unwrap_or_default();
let slice_products = crate::services::user_persistence::UserPersistence::get_slice_products(&user_email);
Ok(ResponseBuilder::ok().json(slice_products).build())
}
/// Create a new slice product
pub async fn create_slice_product(session: Session, slice_data: web::Json<serde_json::Value>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email").unwrap_or_default().unwrap_or_default();
// Parse slice data from JSON
let name = slice_data.get("name").and_then(|v| v.as_str()).unwrap_or("Custom Slice").to_string();
let cpu_cores = slice_data.get("cpu").and_then(|v| v.as_i64()).unwrap_or(2) as i32;
let memory_gb = slice_data.get("ram").and_then(|v| v.as_i64()).unwrap_or(4) as i32;
let storage_gb = slice_data.get("storage").and_then(|v| v.as_i64()).unwrap_or(50) as i32;
let bandwidth_mbps = slice_data.get("bandwidth").and_then(|v| v.as_i64()).unwrap_or(100) as i32;
let min_uptime_sla = slice_data.get("uptime").and_then(|v| v.as_f64()).unwrap_or(99.0) as f32;
let public_ips = slice_data.get("public_ips").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
// Use the actual price from user input instead of hardcoded value
let price_per_hour = slice_data.get("price_hour")
.and_then(|v| v.as_f64())
.map(|p| rust_decimal::Decimal::from_f64_retain(p).unwrap_or(rust_decimal::Decimal::new(50, 2)))
.unwrap_or(rust_decimal::Decimal::new(50, 2)); // Fallback to 0.50 MC/hour only if no price provided
// Load user data to get resource_provider name
let user = Self::load_user_with_persistent_data(&session);
let resource_provider_name = user.as_ref().map(|u| u.name.clone()).unwrap_or_else(|| "Unknown ResourceProvider".to_string());
// Create slice configuration with pricing
let slice_pricing = crate::models::product::SlicePricing::from_hourly(
price_per_hour,
5.0, // 5% daily discount
15.0, // 15% monthly discount
25.0, // 25% yearly discount
);
let slice_config = crate::models::product::SliceConfiguration {
cpu_cores,
memory_gb,
storage_gb,
bandwidth_mbps,
min_uptime_sla,
public_ips,
node_id: None,
slice_type: crate::models::product::SliceType::Basic,
pricing: slice_pricing,
};
// Create slice product
let slice_product = crate::models::product::Product::create_slice_product(
user_email.clone(),
resource_provider_name,
name,
slice_config,
price_per_hour,
);
match crate::services::user_persistence::UserPersistence::save_slice_product(&user_email, slice_product.clone()) {
Ok(_) => {
ResponseBuilder::ok().status(201).json(slice_product).build()
}
Err(e) => {
ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to save slice product",
"details": e
})).build()
}
}
}
/// Delete slice product
pub async fn delete_slice_product(session: Session, path: web::Path<String>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email").unwrap_or_default().unwrap_or_default();
let product_id = path.into_inner();
match crate::services::user_persistence::UserPersistence::delete_slice_product(&user_email, &product_id) {
Ok(_) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"message": "Slice product deleted successfully"
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to delete slice product",
"details": e
})).build())
}
}
}
/// Get slice configuration details
pub async fn get_slice_details(session: Session, path: web::Path<String>) -> Result<impl Responder> {
let slice_id = path.into_inner();
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({"error": "Not authenticated"})).build()),
};
// Load user's slice products
let slice_products = crate::services::user_persistence::UserPersistence::get_user_slice_products(&user_email);
if let Some(slice_product) = slice_products.iter().find(|p| p.id == slice_id) {
// Extract slice configuration from product attributes
let slice_config = slice_product.get_slice_configuration();
let response_data = serde_json::json!({
"id": slice_product.id,
"name": slice_product.name,
"description": slice_product.description,
"base_price": slice_product.base_price,
"base_currency": slice_product.base_currency,
"slice_configuration": slice_config,
"provider_name": slice_product.provider_name,
"availability": slice_product.availability,
"metadata": slice_product.metadata,
"created_at": slice_product.created_at,
"updated_at": slice_product.updated_at
});
Ok(ResponseBuilder::ok().json(response_data).build())
} else {
Ok(ResponseBuilder::not_found().json(serde_json::json!({"error": "Slice configuration not found"})).build())
}
}
/// Update slice configuration
pub async fn update_slice_configuration(session: Session, path: web::Path<String>, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let slice_id = path.into_inner();
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({"error": "Not authenticated"})).build()),
};
let update_data = form.into_inner();
// Load user's slice products
let mut slice_products = crate::services::user_persistence::UserPersistence::get_user_slice_products(&user_email);
if let Some(slice_product) = slice_products.iter_mut().find(|p| p.id == slice_id) {
// Update slice configuration
if let Some(name) = update_data.get("name").and_then(|v| v.as_str()) {
slice_product.name = name.to_string();
}
if let Some(description) = update_data.get("description").and_then(|v| v.as_str()) {
slice_product.description = description.to_string();
}
// Update pricing information
if let Some(pricing) = update_data.get("pricing") {
if let Some(hourly) = pricing.get("hourly").and_then(|v| v.as_f64()) {
slice_product.base_price = rust_decimal::Decimal::try_from(hourly).unwrap_or(slice_product.base_price);
}
}
// Update slice configuration attributes
if let Some(config) = update_data.get("slice_configuration") {
// Extract pricing information from config
let pricing = if let Some(pricing_data) = config.get("pricing") {
crate::models::product::SlicePricing {
hourly: pricing_data.get("hourly")
.and_then(|v| v.as_str())
.and_then(|s| rust_decimal::Decimal::from_str(s).ok())
.unwrap_or_default(),
daily: pricing_data.get("daily")
.and_then(|v| v.as_str())
.and_then(|s| rust_decimal::Decimal::from_str(s).ok())
.unwrap_or_default(),
monthly: pricing_data.get("monthly")
.and_then(|v| v.as_str())
.and_then(|s| rust_decimal::Decimal::from_str(s).ok())
.unwrap_or_default(),
yearly: pricing_data.get("yearly")
.and_then(|v| v.as_str())
.and_then(|s| rust_decimal::Decimal::from_str(s).ok())
.unwrap_or_default(),
}
} else {
crate::models::product::SlicePricing::default()
};
let slice_config = crate::models::product::SliceConfiguration {
cpu_cores: config.get("cpu_cores").and_then(|v| v.as_i64()).unwrap_or(2) as i32,
memory_gb: config.get("memory_gb").and_then(|v| v.as_i64()).unwrap_or(4) as i32,
storage_gb: config.get("storage_gb").and_then(|v| v.as_i64()).unwrap_or(100) as i32,
bandwidth_mbps: config.get("bandwidth_mbps").and_then(|v| v.as_i64()).unwrap_or(100) as i32,
min_uptime_sla: config.get("min_uptime_sla").and_then(|v| v.as_f64()).unwrap_or(99.0) as f32,
public_ips: config.get("public_ips").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
node_id: None,
slice_type: crate::models::product::SliceType::Basic,
pricing,
};
// Update the slice configuration attribute
slice_product.add_attribute(
"slice_configuration".to_string(),
serde_json::to_value(&slice_config).unwrap_or_default(),
crate::models::product::AttributeType::SliceConfiguration,
);
}
slice_product.updated_at = chrono::Utc::now();
// Clone the updated slice for response before saving
let updated_slice = slice_product.clone();
// Save updated slice products
match crate::services::user_persistence::UserPersistence::save_user_slice_products(&user_email, &slice_products) {
Ok(_) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Slice configuration updated successfully",
"slice": updated_slice
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({"error": "Failed to update slice configuration"})).build())
}
}
} else {
Ok(ResponseBuilder::not_found().json(serde_json::json!({"error": "Slice configuration not found"})).build())
}
}
/// Enhanced user data API with real persistent data
pub async fn user_dashboard_data_api(session: Session) -> Result<impl Responder> {
// Get user email from session
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => return ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build()
};
// Load user data using UserService
let user_service = UserService::builder().build().map_err(|e| {
actix_web::error::ErrorInternalServerError("Failed to create user service")
})?;
// Gather comprehensive dashboard data
let applications = user_service.get_user_applications(&user_email);
let services = user_service.get_user_published_services(&user_email);
let compute_resources = user_service.get_user_compute_resources(&user_email);
let recent_activities = user_service.get_user_activities(&user_email, Some(10));
let user_metrics = user_service.calculate_user_metrics(&user_email);
// Get slice rentals from slice rental service
let mut slice_rentals = Vec::new();
if let Ok(slice_rental_service) = crate::services::slice_rental::SliceRentalService::builder().build() {
slice_rentals = slice_rental_service.get_user_slice_rentals(&user_email);
}
// Build comprehensive dashboard data
let dashboard_data = serde_json::json!({
"applications": applications,
"services": services,
"compute_resources": compute_resources,
"slice_rentals": slice_rentals.iter().map(|rental| {
serde_json::json!({
"id": rental.rental_id,
"slice_id": rental.slice_combination_id,
"slice_type": "Standard", // Default type
"deployment_type": "VM", // Default deployment type
"cluster_config": null,
"specs": format!("1 vCPU, 4GB RAM, 200GB Storage"),
"location": "Global",
"status": match rental.slice_allocation.status {
crate::services::slice_calculator::AllocationStatus::Active => "Active",
crate::services::slice_calculator::AllocationStatus::Expired => "Expired",
crate::services::slice_calculator::AllocationStatus::Cancelled => "Cancelled",
},
"monthly_cost": rental.slice_allocation.monthly_cost
})
}).collect::<Vec<_>>(),
"recent_activities": recent_activities,
"user_metrics": user_metrics,
"usd_usage_trend": [10, 15, 12, 18, 22, 25], // Mock data
"resource_utilization": {
"cpu": 65,
"memory": 72,
"storage": 45,
"network": 38
},
"user_activity": {
"deployments": [2, 3, 1, 4, 2, 3],
"resource_reservations": [1, 2, 1, 2, 3, 2]
},
"deployment_distribution": {
"regions": [
{"region": "US-East", "apps": 3, "nodes": 2, "slices": 4},
{"region": "EU-West", "apps": 2, "nodes": 1, "slices": 2},
{"region": "Asia-Pacific", "apps": 1, "nodes": 1, "slices": 1}
]
}
});
ResponseBuilder::ok().json(dashboard_data).build()
}
pub async fn manage_slice_rental(
session: Session,
path: web::Path<String>,
form: web::Json<serde_json::Value>
) -> Result<impl Responder> {
let rental_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => return ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build()
};
let action = form.get("action").and_then(|v| v.as_str()).unwrap_or("");
match action {
"terminate" => {
// Simulate slice rental termination
// In a real implementation, this would:
// 1. Stop all deployments on the slice
// 2. Release the slice allocation
// 3. Update the rental status
// 4. Process any refunds if applicable
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Slice rental {} has been terminated", rental_id)
})).build()
}
"resize" => {
// Simulate slice rental resizing
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Slice rental {} resize initiated", rental_id)
})).build()
}
_ => {
ResponseBuilder::bad_request().json(serde_json::json!({
"error": "Invalid action specified"
})).build()
}
}
}
pub async fn user_data_api(session: Session) -> Result<impl Responder> {
// Build user service with configuration
let user_service = match crate::services::user_service::UserService::builder()
.activity_limit(50)
.build()
{
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to initialize user service"
})).build());
}
};
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
Ok(None) => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Session error"
})).build());
}
};
// Load real data using UserService
let activities = user_service.get_user_activities(&user_email, Some(10));
let purchase_history = user_service.get_purchase_history(&user_email);
let usage_statistics = user_service.get_usage_statistics(&user_email);
let active_deployments = user_service.get_active_deployments(&user_email);
let applications = user_service.get_user_applications(&user_email);
let compute_resources = user_service.get_user_compute_resources(&user_email);
let _metrics = user_service.calculate_user_metrics(&user_email);
// Get user profile info
let user_info = if let Some(persistent_data) = UserPersistence::load_user_data(&user_email) {
crate::models::user::UserInfo {
name: persistent_data.name.unwrap_or_else(|| "User".to_string()),
email: user_email.clone(),
member_since: "2024".to_string(), // TODO: Calculate from first transaction
account_type: "Standard".to_string(),
verification_status: "Verified".to_string(),
}
} else {
crate::models::user::UserInfo {
name: "User".to_string(),
email: user_email.clone(),
member_since: "2024".to_string(),
account_type: "Standard".to_string(),
verification_status: "Unverified".to_string(),
}
};
// Get purchased services for the user (not published services)
let services = user_service.get_user_purchased_services(&user_email);
// Build wallet summary
let wallet_summary = if let Some(persistent_data) = UserPersistence::load_user_data(&user_email) {
crate::models::user::WalletSummary {
balance: persistent_data.wallet_balance_usd,
currency: "USD".to_string(),
recent_transactions: persistent_data.transactions.len() as i32,
pending_transactions: 0, // TODO: Calculate pending transactions
}
} else {
crate::models::user::WalletSummary {
balance: rust_decimal::Decimal::ZERO,
currency: "USD".to_string(),
recent_transactions: 0,
pending_transactions: 0,
}
};
// Generate recommendations based on user data
let recommendations = vec![
crate::models::user::Recommendation {
id: "rec1".to_string(),
title: "Complete Your Profile".to_string(),
description: "Add more information to your profile to get better recommendations".to_string(),
action_url: "/dashboard/settings".to_string(),
priority: "Medium".to_string(),
category: "Profile".to_string(),
},
crate::models::user::Recommendation {
id: "rec2".to_string(),
title: "Explore Marketplace".to_string(),
description: "Discover new applications and services in our marketplace".to_string(),
action_url: "/marketplace".to_string(),
priority: "Low".to_string(),
category: "Discovery".to_string(),
},
];
// Generate quick actions
let quick_actions = vec![
crate::models::user::QuickAction {
id: "action1".to_string(),
label: "Add Funds".to_string(),
description: "Add USD Credits to your wallet".to_string(),
icon: "wallet".to_string(),
url: "/dashboard/wallet".to_string(),
category: "wallet".to_string(),
importance: 1,
last_updated: chrono::Utc::now(),
title: "Add Funds".to_string(),
action_url: "/dashboard/wallet".to_string(),
enabled: true,
},
crate::models::user::QuickAction {
id: "action2".to_string(),
label: "Deploy App".to_string(),
description: "Deploy a new application".to_string(),
icon: "deploy".to_string(),
url: "/marketplace/applications".to_string(),
category: "deployment".to_string(),
importance: 2,
last_updated: chrono::Utc::now(),
title: "Deploy App".to_string(),
action_url: "/marketplace/applications".to_string(),
enabled: true,
},
];
// Build comprehensive dashboard data using existing builder pattern
let dashboard_data = match crate::models::builders::UserDashboardDataBuilder::new()
.user_info(user_info)
.activities(activities.clone())
.statistics(usage_statistics)
.services(services.clone())
.active_deployments(active_deployments.len() as i32)
.wallet_summary(wallet_summary)
.recommendations(recommendations)
.quick_actions(quick_actions)
.build()
{
Ok(data) => data,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to build dashboard data"
})).build());
}
};
// Create enhanced response with applications, services, and compute resources
let mut response_data = serde_json::to_value(dashboard_data).unwrap();
response_data["applications"] = serde_json::to_value(&applications).unwrap();
response_data["services"] = serde_json::to_value(&services).unwrap();
response_data["compute_resources"] = serde_json::to_value(&compute_resources).unwrap();
Ok(ResponseBuilder::ok().json(response_data).build())
}
/// Helper API endpoint to add a new farm node
pub async fn add_farm_node(session: Session, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email").unwrap_or_default().unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
// Extract node data from form
let node_data = form.into_inner();
let node_id = uuid::Uuid::new_v4().to_string();
// Build new farm node
let node = match crate::models::builders::FarmNodeBuilder::new()
.id(node_id)
.name(node_data.get("name").and_then(|v| v.as_str()).unwrap_or("New Node").to_string())
.location(node_data.get("location").and_then(|v| v.as_str()).unwrap_or("Unknown").to_string())
.status(crate::models::user::NodeStatus::Online)
.capacity(crate::models::user::NodeCapacity {
cpu_cores: node_data.get("cpu_cores").and_then(|v| v.as_i64()).unwrap_or(4) as i32,
memory_gb: node_data.get("memory_gb").and_then(|v| v.as_i64()).unwrap_or(8) as i32,
storage_gb: node_data.get("storage_gb").and_then(|v| v.as_i64()).unwrap_or(100) as i32,
bandwidth_mbps: node_data.get("bandwidth_mbps").and_then(|v| v.as_i64()).unwrap_or(100) as i32,
ssd_storage_gb: node_data.get("storage_gb").and_then(|v| v.as_i64()).unwrap_or(100) as i32, // Default to SSD
hdd_storage_gb: 0,
ram_gb: node_data.get("memory_gb").and_then(|v| v.as_i64()).unwrap_or(8) as i32,
})
.uptime_percentage(99.5)
.earnings_today_usd(rust_decimal::Decimal::ZERO)
.health_score(100.0)
.region(node_data.get("region").and_then(|v| v.as_str()).unwrap_or("Global").to_string())
.node_type(node_data.get("node_type").and_then(|v| v.as_str()).unwrap_or("MyceliumNode").to_string())
.build()
{
Ok(node) => node,
Err(e) => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"error": "Invalid node data",
"details": e
})).build());
}
};
// Add node to persistence
match UserPersistence::add_user_node(&user_email, node) {
Ok(()) => {
// Add activity record
let activity = crate::models::builders::UserActivityBuilder::new()
.activity_type(crate::models::user::ActivityType::NodeAdded)
.description("Added new farm node".to_string())
.category("Farming".to_string())
.importance(crate::models::user::ActivityImportance::High)
.build()
.unwrap();
let _ = UserPersistence::add_user_activity(&user_email, activity);
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Farm node added successfully"
})).build())
},
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to add farm node",
"details": e.to_string()
})).build())
}
}
}
/// API endpoint to validate grid nodes
pub async fn validate_grid_nodes(session: Session, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
// Parse node IDs from request
let node_ids: Vec<u32> = match form.get("node_ids") {
Some(serde_json::Value::Array(ids)) => {
ids.iter()
.filter_map(|id| id.as_u64().map(|n| n as u32))
.collect()
}
_ => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"error": "Invalid node_ids format"
})).build());
}
};
if node_ids.is_empty() {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"error": "No node IDs provided"
})).build());
}
// Initialize grid service
let grid_service = match crate::services::grid::GridService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to initialize grid service"
})).build());
}
};
// Initialize resource_provider service to check for existing nodes
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to initialize resource_provider service"
})).build());
}
};
// Get existing nodes to check for duplicates
let existing_nodes = resource_provider_service.get_resource_provider_nodes(&user_email);
// Validate each node and fetch data
let mut validated_nodes = Vec::new();
let mut errors = Vec::new();
for node_id in &node_ids {
// Check if node is already registered
if existing_nodes.iter().any(|n| n.grid_node_id == Some(node_id.to_string())) {
errors.push(serde_json::json!({
"node_id": *node_id,
"error": format!("Node {} is already registered", *node_id)
}));
continue;
}
match grid_service.fetch_node_data(*node_id).await {
Ok(node_data) => {
validated_nodes.push(serde_json::json!({
"node_id": *node_id,
"valid": true,
"data": node_data
}));
}
Err(e) => {
errors.push(serde_json::json!({
"node_id": *node_id,
"error": e
}));
}
}
}
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"validated_nodes": validated_nodes,
"errors": errors,
"total_requested": node_ids.len(),
"total_valid": validated_nodes.len()
})).build())
}
/// API endpoint to add grid nodes
pub async fn add_grid_nodes(session: Session, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
// Structured logging: request context
let req_id = Uuid::new_v4().to_string();
let start_total = Instant::now();
log::info!(
target: "api.dashboard",
"add_grid_nodes:start req_id={} email={}",
req_id,
user_email
);
// Acquire per-user async lock to serialize modifications for this user
let user_lock = UserPersistence::get_user_lock(&user_email);
// Optional: log when waiting for the lock
log::debug!(target: "concurrency", "add_grid_nodes:waiting_for_lock req_id={} email={}", req_id, user_email);
let lock_wait_start = Instant::now();
let _lock_guard = user_lock.lock().await;
let lock_wait_ms = lock_wait_start.elapsed().as_millis();
log::info!(target: "concurrency", "add_grid_nodes:lock_acquired req_id={} email={} wait_ms={}", req_id, user_email, lock_wait_ms);
// Parse request data
let node_ids: Vec<u32> = match form.get("node_ids") {
Some(serde_json::Value::Array(ids)) => {
ids.iter()
.filter_map(|id| id.as_u64().map(|n| n as u32))
.collect()
}
_ => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"error": "Invalid node_ids format"
})).build());
}
};
let group_config = form.get("group_config");
let slice_configuration = form.get("slice_configuration");
let node_group_id = form.get("node_group_id").and_then(|v| v.as_str()).map(|s| s.to_string());
let full_node_rental_config = form.get("full_node_rental_config");
let _multi_node_pricing = form.get("multi_node_pricing").and_then(|v| v.as_str()).unwrap_or("single");
let staking_configuration = form.get("staking_configuration");
// Extract slice formats and rental options from new configuration
let slice_formats = if let Some(config) = slice_configuration {
config.get("slice_formats")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect::<Vec<String>>())
.unwrap_or_default()
} else {
Vec::new()
};
let slice_rental_enabled = slice_configuration
.and_then(|config| config.get("slice_rental_enabled"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
// Parse comprehensive full node rental configuration
let (full_node_rental_enabled, full_node_pricing, individual_node_pricing, minimum_rental_days, pricing_mode) = if let Some(config) = full_node_rental_config {
let enabled = config.get("full_node_rental_enabled").and_then(|v| v.as_bool()).unwrap_or(false);
let min_days = config.get("minimum_rental_days").and_then(|v| v.as_u64()).unwrap_or(30) as u32;
let pricing_mode = config.get("pricing_mode").and_then(|v| v.as_str()).unwrap_or("same_for_all");
if enabled && pricing_mode == "individual" {
// Handle individual node pricing
let individual_pricing = if let Some(individual_data) = config.get("individual_node_pricing") {
if let Some(individual_obj) = individual_data.as_object() {
let mut pricing_map = std::collections::HashMap::new();
for (node_key, pricing_data) in individual_obj {
if let Some(pricing_obj) = pricing_data.as_object() {
let hourly = pricing_obj.get("hourly")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let daily = pricing_obj.get("daily")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let monthly = pricing_obj.get("monthly")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let yearly = pricing_obj.get("yearly")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let auto_calculate = pricing_obj.get("auto_calculate")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let daily_discount_percent = pricing_obj.get("daily_discount_percent")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let monthly_discount_percent = pricing_obj.get("monthly_discount_percent")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let yearly_discount_percent = pricing_obj.get("yearly_discount_percent")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
// Create pricing using builder pattern
match crate::models::builders::FullNodePricingBuilder::new()
.hourly(hourly)
.daily(daily)
.monthly(monthly)
.yearly(yearly)
.auto_calculate(auto_calculate)
.daily_discount_percent(daily_discount_percent)
.monthly_discount_percent(monthly_discount_percent)
.yearly_discount_percent(yearly_discount_percent)
.build() {
Ok(pricing) => {
pricing_map.insert(node_key.clone(), pricing);
},
Err(e) => {
}
}
}
}
Some(pricing_map)
} else {
None
}
} else {
None
};
(enabled, None, individual_pricing, min_days, pricing_mode.to_string())
} else {
// Handle standard pricing (same for all nodes)
let pricing = if enabled {
if let Some(pricing_data) = config.get("full_node_pricing") {
if let Some(pricing_obj) = pricing_data.as_object() {
let hourly = pricing_obj.get("hourly")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let daily = pricing_obj.get("daily")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let monthly = pricing_obj.get("monthly")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let yearly = pricing_obj.get("yearly")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let auto_calculate = pricing_obj.get("auto_calculate")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let daily_discount_percent = pricing_obj.get("daily_discount_percent")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let monthly_discount_percent = pricing_obj.get("monthly_discount_percent")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let yearly_discount_percent = pricing_obj.get("yearly_discount_percent")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
// Create pricing using builder pattern
match crate::models::builders::FullNodePricingBuilder::new()
.hourly(hourly)
.daily(daily)
.monthly(monthly)
.yearly(yearly)
.auto_calculate(auto_calculate)
.daily_discount_percent(daily_discount_percent)
.monthly_discount_percent(monthly_discount_percent)
.yearly_discount_percent(yearly_discount_percent)
.build() {
Ok(pricing) => Some(pricing),
Err(e) => {
None
}
}
} else {
None
}
} else {
None
}
} else {
None
};
(enabled, pricing, None, min_days, pricing_mode.to_string())
}
} else {
(false, None, None, 30, "same_for_all".to_string())
};
// Legacy support for old format
let slice_format = form.get("slice_format").and_then(|v| v.as_str()).map(|s| s.to_string());
let slice_price = form.get("slice_price").and_then(|v| v.as_f64()).map(|p| Decimal::from_f64_retain(p).unwrap_or_default());
// Initialize resource_provider service
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to initialize resource_provider service"
})).build());
}
};
// Add nodes with comprehensive configuration
log::info!(
target: "api.dashboard",
"add_grid_nodes:invoke_resource_provider_service req_id={} email={} node_count={} slice_enabled={} full_node_enabled={} pricing_mode={}",
req_id,
user_email,
node_ids.len(),
slice_rental_enabled,
full_node_rental_enabled,
pricing_mode
);
let add_result = if !slice_formats.is_empty() || full_node_rental_enabled {
// Create rental options if full node rental is enabled
let rental_options = if full_node_rental_enabled {
match crate::models::builders::NodeRentalOptionsBuilder::new()
.slice_rental_enabled(slice_rental_enabled)
.full_node_rental_enabled(full_node_rental_enabled)
.full_node_pricing(full_node_pricing.unwrap_or_else(|| {
crate::models::user::FullNodePricing {
monthly_cost: rust_decimal::Decimal::new(50, 0),
setup_fee: Some(rust_decimal::Decimal::new(10, 0)),
deposit_required: Some(rust_decimal::Decimal::new(100, 0)),
..Default::default()
}
}))
.minimum_rental_days(minimum_rental_days)
.auto_renewal_enabled(false)
.build() {
Ok(options) => Some(options),
Err(e) => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"error": "Failed to create rental options",
"details": e
})).build());
}
}
} else {
None
};
// For multi-node scenarios, apply pricing configuration based on user choice
if node_ids.len() > 1 && pricing_mode == "same_for_all" && rental_options.is_some() {
} else if node_ids.len() > 1 && pricing_mode == "individual" && individual_node_pricing.is_some() {
// Individual pricing will be handled in the resource_provider service
}
// Choose the appropriate method based on pricing mode
if pricing_mode == "individual" && individual_node_pricing.is_some() {
resource_provider_service.add_multiple_grid_nodes_with_individual_pricing(
&user_email,
node_ids.clone(),
slice_formats,
individual_node_pricing.unwrap()
).await
} else {
resource_provider_service.add_multiple_grid_nodes_with_comprehensive_config(
&user_email,
node_ids.clone(),
slice_formats,
rental_options
).await
}
} else {
resource_provider_service.add_multiple_grid_nodes(&user_email, node_ids.clone()).await
};
match add_result {
Ok(added_nodes) => {
let total_ms = start_total.elapsed().as_millis();
log::info!(
target: "api.dashboard",
"add_grid_nodes:success req_id={} email={} added_count={} total_ms={}",
req_id,
user_email,
added_nodes.len(),
total_ms
);
// If node_group_id is provided, assign nodes to existing group
if let Some(group_id) = node_group_id {
for node in &added_nodes {
if let Err(e) = resource_provider_service.assign_node_to_group(&user_email, &node.id, Some(group_id.clone())) {
} else {
}
}
}
// If group configuration is provided, create group and add nodes
else if let Some(group_data) = group_config {
if let (Some(group_name), Some(group_description)) = (
group_data.get("name").and_then(|v| v.as_str()),
group_data.get("description").and_then(|v| v.as_str())
) {
// Create node group
match resource_provider_service.create_custom_node_group(
&user_email,
group_name.to_string(),
Some(group_description.to_string()),
Some(crate::models::user::NodeGroupConfig {
group_name: group_name.to_string(),
max_nodes: 10, // Default max nodes
allocation_strategy: "balanced".to_string(),
auto_scaling: false,
preferred_slice_formats: vec![slice_format.clone().unwrap_or_else(|| "performance".to_string())],
default_pricing: slice_price.map(|p| {
serde_json::to_value(p).unwrap_or_default()
}),
resource_optimization: crate::models::user::ResourceOptimization::default(),
})
) {
Ok(group) => {
// Add nodes to group
for node in &added_nodes {
if let Err(e) = resource_provider_service.assign_node_to_group(&user_email, &node.id, Some(group.id.clone())) {
}
}
}
Err(e) => {
}
}
}
}
// Handle staking configuration if provided
if let Some(staking_config) = staking_configuration {
if let Some(staking_enabled) = staking_config.get("staking_enabled").and_then(|v| v.as_bool()) {
if staking_enabled {
// Check if it's individual staking or same for all
if let Some(individual_config) = staking_config.get("individual_staking") {
// Handle individual staking per node
if let Some(individual_obj) = individual_config.as_object() {
for node in &added_nodes {
let node_key = format!("grid_{}", node.grid_node_id.clone().unwrap_or_default());
if let Some(node_staking) = individual_obj.get(&node_key) {
if let Some(staked_amount) = node_staking.get("staked_amount").and_then(|v| v.as_f64()) {
let staking_period = node_staking.get("staking_period_months").and_then(|v| v.as_u64()).unwrap_or(12) as u32;
let staking_options = match NodeStakingOptionsBuilder::new()
.staking_enabled(true)
.staked_amount(Decimal::from_str(&staked_amount.to_string()).unwrap_or(Decimal::ZERO))
.staking_period_months(staking_period)
.early_withdrawal_allowed(true)
.build() {
Ok(options) => options,
Err(e) => {
continue;
}
};
if let Err(e) = resource_provider_service.stake_on_node(&user_email, &node.id, staking_options) {
} else {
}
}
}
}
}
} else {
// Handle same staking for all nodes
if let Some(staked_amount) = staking_config.get("staked_amount").and_then(|v| v.as_f64()) {
let staking_period = staking_config.get("staking_period_months").and_then(|v| v.as_u64()).unwrap_or(12) as u32;
let staking_options = match NodeStakingOptionsBuilder::new()
.staking_enabled(true)
.staked_amount(Decimal::from_str(&staked_amount.to_string()).unwrap_or(Decimal::ZERO))
.staking_period_months(staking_period)
.early_withdrawal_allowed(true)
.build() {
Ok(options) => options,
Err(e) => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"error": "Failed to create staking options",
"details": e
})).build());
}
};
for node in &added_nodes {
if let Err(e) = resource_provider_service.stake_on_node(&user_email, &node.id, staking_options.clone()) {
} else {
}
}
}
}
}
}
}
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"added_nodes": added_nodes,
"node_ids": node_ids
})).build())
}
Err(_e) => {
let total_ms = start_total.elapsed().as_millis();
log::error!(
target: "api.dashboard",
"add_grid_nodes:error req_id={} email={} err={} total_ms={}",
req_id,
user_email,
_e,
total_ms
);
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to add grid nodes",
"details": _e.to_string()
})).build())
}
}
}
/// API endpoint to create node group
pub async fn create_node_group(session: Session, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"error": "User not authenticated"
})).build());
}
let name = match form.get("name").and_then(|v| v.as_str()) {
Some(n) => n,
None => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"error": "Group name is required"
})).build());
}
};
let description = form.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
let slice_format = form.get("slice_format").and_then(|v| v.as_str()).map(|s| s.to_string());
let slice_price = form.get("slice_price").and_then(|v| v.as_f64()).map(|p| Decimal::from_f64_retain(p).unwrap_or_default());
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(_e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to initialize resource_provider service"
})).build());
}
};
// Convert old API to new custom group creation
let config = crate::models::user::NodeGroupConfig {
group_name: name.to_string(),
max_nodes: 10, // Default max nodes
allocation_strategy: "balanced".to_string(),
auto_scaling: false,
preferred_slice_formats: vec![slice_format.unwrap_or_else(|| "performance".to_string())],
default_pricing: slice_price.map(|p| {
serde_json::to_value(p).unwrap_or_default()
}),
resource_optimization: crate::models::user::ResourceOptimization::default(),
};
match resource_provider_service.create_custom_node_group(
&user_email,
name.to_string(),
description,
Some(config)
) {
Ok(group) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Node group created successfully",
"group": group
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to create node group",
"details": e
})).build())
}
}
}
/// API endpoint to get all node groups with statistics
pub async fn get_node_groups_api(session: Session) -> Result<impl Responder> {
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
Ok(None) => return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "User not logged in"
})).build()),
Err(_) => return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Session error"
})).build()),
};
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": format!("Service initialization failed: {}", e)
})).build()),
};
// RESOURCE_PROVIDER FIX: Repair node-group data consistency before getting statistics
if let Err(e) = resource_provider_service.repair_node_group_consistency(&user_email) {
}
let groups = resource_provider_service.get_node_groups(&user_email);
let mut groups_with_stats = Vec::new();
for group in groups {
match resource_provider_service.get_group_statistics(&user_email, &group.id) {
Ok(stats) => {
groups_with_stats.push(serde_json::json!({
"group": group,
"stats": stats
}));
}
Err(e) => {
groups_with_stats.push(serde_json::json!({
"group": group,
"stats": null
}));
}
}
}
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"groups": groups_with_stats
})).build())
}
/// API endpoint to return application provider dashboard data as JSON
pub async fn application_provider_data_api(session: Session) -> Result<impl Responder> {
// Get user email for debugging
let user_email = session.get::<String>("user_email")
.unwrap_or(None)
.unwrap_or_else(|| "unknown".to_string());
// Load fresh persistent apps directly to ensure we get latest updates
let mut fresh_apps = UserPersistence::get_user_apps(&user_email);
// CROSS-USER DEPLOYMENT COUNT FIX: Count deployments across all users
let cross_user_deployment_counts = Self::count_cross_user_deployments(&user_email);
// Update app deployment counts with cross-user data
for app in &mut fresh_apps {
if let Some(&count) = cross_user_deployment_counts.get(&app.id) {
app.deployments = count;
}
}
let fresh_deployments = UserPersistence::get_user_application_deployments(&user_email);
// Load user persistent data
if let Some(user) = Self::load_user_with_persistent_data(&session) {
let persistent_data = UserPersistence::load_user_data(&user.email).unwrap_or_default();
// Create app provider data from persistent data
let total_deployments = fresh_apps.iter().map(|a| a.deployments).sum::<i32>();
let user_published_app_ids: std::collections::HashSet<String> = fresh_apps.iter()
.map(|app| app.id.clone())
.collect();
let active_deployments = fresh_deployments.iter()
.filter(|d| d.status == "Active" && user_published_app_ids.contains(&d.app_id))
.count() as i32;
let monthly_revenue_usd = fresh_apps.iter().map(|a| {
use std::str::FromStr;
let decimal_val: rust_decimal::Decimal = a.monthly_revenue_usd;
i32::from_str(&decimal_val.to_string()).unwrap_or(0)
}).sum::<i32>();
let deployment_stats: Vec<crate::models::user::DeploymentStat> = fresh_deployments.iter()
.filter(|d| user_published_app_ids.contains(&d.app_id))
.map(|d| {
// Get auto_healing from parent app since AppDeployment doesn't have this field
let auto_healing = fresh_apps.iter()
.find(|app| app.id == d.app_id)
.and_then(|app| app.auto_healing)
.unwrap_or(false);
crate::models::user::DeploymentStat {
app_name: d.app_name.clone(),
region: d.region.clone(),
active_instances: d.instances,
total_instances: d.instances,
avg_response_time_ms: Some(100.0), // Default response time
uptime_percentage: Some(99.5), // Default uptime
status: d.status.clone(),
instances: d.instances,
resource_usage: Some(serde_json::to_string(&d.resource_usage).unwrap_or_default()),
customer_name: Some(d.customer_name.clone()),
deployed_date: {
use chrono::DateTime;
DateTime::parse_from_rfc3339(&d.deployed_date)
.or_else(|_| DateTime::parse_from_str(&d.deployed_date, "%Y-%m-%d %H:%M:%S"))
.map(|dt| dt.with_timezone(&chrono::Utc))
.ok()
},
deployment_id: Some(d.id.clone()),
last_deployment: {
use chrono::DateTime;
DateTime::parse_from_rfc3339(&d.deployed_date)
.or_else(|_| DateTime::parse_from_str(&d.deployed_date, "%Y-%m-%d %H:%M:%S"))
.map(|dt| dt.with_timezone(&chrono::Utc))
.ok()
},
auto_healing: d.auto_healing,
}
}).collect();
let application_provider_data = crate::models::user::AppProviderData {
apps: fresh_apps.clone(),
published_apps: fresh_apps.len() as i32,
total_deployments,
active_deployments,
monthly_revenue_usd,
total_revenue_usd: monthly_revenue_usd * 12, // Estimate annual
deployment_stats,
revenue_history: Vec::new(),
};
return Ok(ResponseBuilder::ok().json(application_provider_data).build());
} else {
}
// Return data based on persistent apps even if no mock data
if !fresh_apps.is_empty() {
let total_deployments = fresh_apps.iter().map(|a| a.deployments).sum::<i32>();
let monthly_revenue = fresh_apps.iter().map(|a| a.monthly_revenue_usd.to_string().parse::<i32>().unwrap_or(0)).sum::<i32>();
return Ok(ResponseBuilder::ok().json(serde_json::json!({
"published_apps": fresh_apps.len(),
"total_deployments": total_deployments,
"active_deployments": fresh_deployments.iter().filter(|d| d.status == "Active").count(),
"monthly_revenue_usd": monthly_revenue,
"total_revenue_usd": monthly_revenue * 12,
"apps": fresh_apps,
"deployment_stats": fresh_deployments.iter().map(|d| {
// Use auto_healing directly from deployment data, or fall back to parent app
let auto_healing = d.auto_healing.unwrap_or_else(|| {
fresh_apps.iter()
.find(|app| app.id == d.app_id)
.and_then(|app| app.auto_healing)
.unwrap_or(false)
});
serde_json::json!({
"app_name": d.app_name,
"region": d.region,
"instances": d.instances,
"status": d.status,
"resource_usage": d.resource_usage,
"customer_name": d.customer_name,
"deployed_date": d.deployed_date,
"deployment_id": d.id,
"auto_healing": auto_healing
})
}).collect::<Vec<_>>(),
"revenue_history": []
})).build());
}
// Return empty app provider data if no user or app provider data
Ok(ResponseBuilder::ok().json(serde_json::json!({
"published_apps": 0,
"total_deployments": 0,
"active_deployments": 0,
"monthly_revenue_usd": 0,
"total_revenue_usd": 0,
"apps": [],
"deployment_stats": [],
"revenue_history": []
})).build())
}
/// API endpoint to return service provider dashboard data as JSON
pub async fn service_provider_data_api(session: Session) -> Result<impl Responder> {
// Get user email for debugging
let user_email = session.get::<String>("user_email")
.unwrap_or(None)
.unwrap_or_else(|| "unknown".to_string());
// PHASE 1 FIX: Enhanced data loading with better error handling and consistency
// Load fresh persistent services directly to ensure we get latest updates
let fresh_services = UserPersistence::get_user_services(&user_email);
for service in &fresh_services {
}
// Load fresh persistent service requests
let fresh_requests = UserPersistence::get_user_service_requests(&user_email);
// New users start with empty service requests - no automatic mock data
for request in &fresh_requests {
}
// PHASE 1 FIX: Build service provider data directly from persistent storage
// This ensures we always return the most up-to-date data
let service_provider_data = crate::models::user::ServiceProviderData {
active_services: fresh_services.len() as i32,
total_clients: fresh_services.iter().map(|s| s.clients).sum::<i32>(),
monthly_revenue_usd: fresh_services.iter().map(|s| s.price_per_hour_usd * s.total_hours.unwrap_or(0)).sum::<i32>(),
total_revenue_usd: fresh_services.iter().map(|s| s.price_per_hour_usd * s.total_hours.unwrap_or(0) * 2).sum::<i32>(), // Estimate
service_rating: if fresh_services.is_empty() { 0.0 } else {
fresh_services.iter().map(|s| s.rating).sum::<f32>() / fresh_services.len() as f32
},
services: fresh_services,
client_requests: fresh_requests,
availability: Some(true), // Default to available
hourly_rate_range: Some("$50-$150".to_string()), // Default range
last_payment_method: Some("Credit Card".to_string()), // Default payment method
revenue_history: Vec::new(), // Can be enhanced later
};
Ok(ResponseBuilder::ok().json(service_provider_data).build())
}
/// Update user profile information
pub async fn update_profile(
form: web::Form<ProfileUpdateForm>,
session: Session,
) -> Result<impl Responder> {
// Get current user from session
if let Some(mut user) = Self::load_user_with_persistent_data(&session) {
// Update user name and profile fields
user.name = form.name.clone();
user.country = Some(form.country.clone());
user.timezone = Some(form.timezone.clone());
// Save profile changes permanently using UserPersistence
if let Err(e) = UserPersistence::update_user_profile(
&user.email,
Some(form.name.clone()),
Some(form.country.clone()),
Some(form.timezone.clone())
) {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Failed to save profile changes"
})).build();
} else {
}
// Update session with essential user data only (avoid serializing complex MockUserData)
// Create a lightweight user object for session storage
let mut session_user_builder = User::builder()
.name(user.name.clone())
.email(user.email.clone())
.role(user.role.clone());
if let Some(id) = user.id {
session_user_builder = session_user_builder.id(id);
}
if let Some(country) = &user.country {
session_user_builder = session_user_builder.country(country.clone());
}
if let Some(timezone) = &user.timezone {
session_user_builder = session_user_builder.timezone(timezone.clone());
}
let session_user = session_user_builder.build().unwrap();
// Update session with lightweight user data
match serde_json::to_string(&session_user) {
Ok(user_json) => {
if let Err(e) = session.insert("user", user_json) {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Failed to update session"
})).build();
}
// Also store preferences separately for easy access
if let Err(e) = session.insert("user_country", &form.country) {
}
if let Err(e) = session.insert("user_timezone", &form.timezone) {
}
},
Err(e) => {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Failed to serialize user data"
})).build();
}
}
// Return success response with updated user data for immediate UI update
let response = ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Profile updated successfully",
"user": {
"name": form.name,
"country": form.country,
"timezone": form.timezone
}
})).build();
return response;
}
let error_response = ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "User not found"
})).build();
error_response
}
/// Update user password
pub async fn update_password(
form: web::Form<PasswordUpdateForm>,
session: Session,
) -> Result<impl Responder> {
// Get current user from session
if let Some(user) = Self::load_user_with_persistent_data(&session) {
// In a real app, we'd verify the current password
// For mock data, we'll just validate the new password format
// Validate password strength
if form.new_password.len() < 12 {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
if form.new_password != form.confirm_password {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "Passwords do not match"
})).build());
}
// Check password complexity
let has_letter = form.new_password.chars().any(|c| c.is_alphabetic());
let has_number = form.new_password.chars().any(|c| c.is_numeric());
let has_special = form.new_password.chars().any(|c| !c.is_alphanumeric());
if !has_letter || !has_number || !has_special {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "Password must contain letters, numbers, and special characters"
})).build());
}
// Hash the password before storing
use bcrypt::{hash, DEFAULT_COST};
let password_hash = match hash(&form.new_password, DEFAULT_COST) {
Ok(hash) => hash,
Err(_) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Failed to process password"
})).build());
}
};
// Store password update persistently using UserPersistence
if let Err(e) = UserPersistence::update_user_password(&user.email, password_hash.clone()) {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Failed to save password changes"
})).build());
}
// Also store in session for immediate use during current session
let _ = session.insert("password_updated", true);
let _ = session.insert("password_update_time", chrono::Utc::now().to_rfc3339());
let _ = session.insert("password_hash", &password_hash);
let _ = session.insert("updated_password", &form.new_password);
return Ok(ResponseBuilder::ok()
.json(serde_json::json!({
"success": true,
"message": "Password updated successfully"
})).build());
}
Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "User not found"
})).build())
}
/// Update notification preferences
pub async fn update_notifications(
form: web::Form<NotificationUpdateForm>,
session: Session,
) -> Result<impl Responder> {
use crate::services::user_persistence::UserPersistence;
use crate::models::user::NotificationSettings;
// Get current user from session
if let Some(user) = Self::load_user_with_persistent_data(&session) {
// Create notification settings from form data
let notification_settings = NotificationSettings {
email_enabled: form.email_security_alerts,
push_enabled: form.dashboard_alerts,
sms_enabled: false, // Not in form, use default
slack_webhook: None, // Not in form, use default
discord_webhook: None, // Not in form, use default
enabled: form.email_newsletter,
push: form.dashboard_updates,
node_offline_alerts: form.email_system_alerts,
maintenance_reminders: form.dashboard_updates,
earnings_reports: form.email_billing_alerts,
};
// Store notification preferences persistently using UserPersistence
if let Err(e) = UserPersistence::update_notification_settings(&user.email, notification_settings) {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Failed to save notification settings"
})).build());
}
// Also store in session for immediate use during current session
let _ = session.insert("email_security_alerts", form.email_security_alerts);
let _ = session.insert("email_billing_alerts", form.email_billing_alerts);
let _ = session.insert("email_system_alerts", form.email_system_alerts);
let _ = session.insert("email_newsletter", form.email_newsletter);
let _ = session.insert("dashboard_alerts", form.dashboard_alerts);
let _ = session.insert("dashboard_updates", form.dashboard_updates);
let _ = session.insert("notification_preferences_updated", chrono::Utc::now().to_rfc3339());
return Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Notification preferences updated successfully"
})).build());
}
Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"message": "User not found"
})).build())
}
/// Add MC Credits to user balance
pub async fn add_tfp(
form: web::Form<AddTfpForm>,
session: Session,
) -> Result<impl Responder> {
// Get current user from session
if let Some(user) = Self::load_user_with_persistent_data(&session) {
let user_email = user.email.clone();
let mut persistent_data = UserPersistence::load_user_data(&user_email).unwrap_or_default();
// Add USD Credits to balance
let amount_to_add = Decimal::from(form.amount);
persistent_data.wallet_balance_usd += amount_to_add;
// Add transaction record
let transaction = crate::models::user::Transaction {
id: Uuid::new_v4().to_string(),
user_id: user_email.clone(),
transaction_type: crate::models::user::TransactionType::Purchase {
product_id: "manual_addition".to_string()
},
amount: amount_to_add,
currency: Some("USD".to_string()),
exchange_rate_usd: Some(rust_decimal::Decimal::ONE),
amount_usd: Some(amount_to_add),
description: Some(format!("Manual wallet top-up of ${}", form.amount)),
reference_id: None,
metadata: None,
timestamp: chrono::Utc::now(),
status: crate::models::user::TransactionStatus::Completed,
};
persistent_data.transactions.push(transaction);
let new_balance = persistent_data.wallet_balance_usd;
// Save updated persistent data
if let Err(_e) = UserPersistence::save_user_data(&persistent_data) {
return Ok(ResponseBuilder::error()
.json(serde_json::json!({
"success": false,
"message": "Failed to save wallet update"
}))
.build()?);
}
return Ok(ResponseBuilder::ok()
.json(serde_json::json!({
"success": true,
"message": format!("Successfully added ${} USD Credits to your account", form.amount),
"new_balance": new_balance.to_f64().unwrap_or(0.0)
}))
.build()?);
}
Ok(ResponseBuilder::bad_request()
.json(serde_json::json!({
"success": false,
"message": "User not found or invalid amount"
}))
.build()?)
}
/// Verify user password (pre-check for account deletion)
pub async fn verify_password(
form: web::Form<VerifyPasswordForm>,
session: Session,
) -> Result<impl Responder> {
use crate::services::user_persistence::UserPersistence;
// Do NOT log raw password
log::info!(
target: "account_deletion",
"verify_password:received pwd_len={}",
form.password.len()
);
if form.password.trim().is_empty() {
return Ok(ResponseBuilder::bad_request()
.message("Enter your current password.")
.build());
}
if let Some(user) = Self::load_user_with_persistent_data(&session) {
let persistent_data = UserPersistence::load_user_data(&user.email).unwrap_or_default();
if let Some(stored_hash) = &persistent_data.password_hash {
match verify(&form.password, stored_hash) {
Ok(true) => {
log::info!(target: "account_deletion", "verify_password:success email={}", user.email);
return Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "Password verified."
})).build());
}
Ok(false) => {
log::warn!(target: "account_deletion", "verify_password:incorrect email={}", user.email);
return Ok(ResponseBuilder::bad_request()
.message("The password you entered is incorrect. Please try again.")
.build());
}
Err(_) => {
log::error!(target: "account_deletion", "verify_password:error email={}", user.email);
return Ok(ResponseBuilder::internal_error()
.message("Failed to verify password")
.build());
}
}
} else {
return Ok(ResponseBuilder::bad_request()
.message("This account does not have a password set. Please set a password in Settings > Password before deleting your account.")
.build());
}
}
Ok(ResponseBuilder::bad_request()
.message("User not found in session")
.build())
}
/// Delete user account
pub async fn delete_account(
form: web::Form<DeleteAccountForm>,
session: Session,
) -> Result<impl Responder> {
use crate::services::user_persistence::UserPersistence;
// Trace incoming request (do NOT log raw password)
log::info!(
target: "account_deletion",
"delete_account:received confirmation={} pwd_len={}",
form.confirmation,
form.password.len()
);
// Validate confirmation text
if form.confirmation.to_uppercase() != "DELETE" {
return Ok(ResponseBuilder::bad_request()
.message("Invalid confirmation. Please type DELETE to confirm account deletion.")
.build());
}
// Get current user from session
if let Some(user) = Self::load_user_with_persistent_data(&session) {
log::info!(
target: "account_deletion",
"delete_account:user_loaded email={}",
user.email
);
// Load persistent data to access password hash
let persistent_data = UserPersistence::load_user_data(&user.email).unwrap_or_default();
// Verify password if user has one set; otherwise block deletion until a password is set
if let Some(stored_hash) = &persistent_data.password_hash {
log::info!(
target: "account_deletion",
"delete_account:password_hash_present email={} hash_prefix={}",
user.email,
&stored_hash.chars().take(7).collect::<String>()
);
if form.password.trim().is_empty() {
return Ok(ResponseBuilder::bad_request()
.message("Enter your current password.")
.build());
}
match verify(&form.password, stored_hash) {
Ok(true) => {
// Password is correct, proceed
log::info!(
target: "account_deletion",
"delete_account:password_verify=success email={}",
user.email
);
},
Ok(false) => {
log::warn!(
target: "account_deletion",
"delete_account:password_verify=incorrect email={} pwd_len={}",
user.email,
form.password.len()
);
return Ok(ResponseBuilder::bad_request()
.message("The password you entered is incorrect. Please try again.")
.build());
},
Err(_) => {
log::error!(
target: "account_deletion",
"delete_account:password_verify=error email={}",
user.email
);
return Ok(ResponseBuilder::internal_error()
.message("Failed to verify password")
.build());
}
}
} else {
log::warn!(
target: "account_deletion",
"delete_account:no_password_set email={}",
user.email
);
// For accounts without a password set (e.g., created via SSO), require setting a password first
return Ok(ResponseBuilder::bad_request()
.message("This account does not have a password set. Please set a password in Settings > Password before deleting your account.")
.build());
}
// Perform soft delete - mark account as deleted instead of removing data
match UserPersistence::soft_delete_user_account(&user.email, Some("User requested account deletion".to_string())) {
Ok(()) => {
log::info!(
target: "account_deletion",
"delete_account:soft_delete_success email={}",
user.email
);
// Clear all session data to log out the user
session.clear();
return Ok(ResponseBuilder::ok()
.json(serde_json::json!({
"success": true,
"message": "Account deleted successfully. All your data has been preserved for potential recovery.",
"redirect": "/dashboard"
}))
.build());
},
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": format!("Failed to delete account: {}", e)
})).build());
}
}
}
Ok(ResponseBuilder::bad_request()
.json(serde_json::json!({
"success": false,
"message": "User not found in session"
}))
.build())
}
/// Get billing history for the user
pub async fn get_billing_history(session: Session) -> Result<impl Responder> {
// Get current user from session
if let Some(user) = Self::load_user_with_persistent_data(&session) {
// Create mock billing history
let billing_history = vec![
serde_json::json!({
"id": "bill_001",
"date": "2024-05-01",
"description": "Monthly USD Credits Usage",
"amount": 450,
"status": "Paid",
"invoice_url": "/invoices/bill_001.pdf"
}),
serde_json::json!({
"id": "bill_002",
"date": "2024-04-01",
"description": "Monthly USD Credits Usage",
"amount": 380,
"status": "Paid",
"invoice_url": "/invoices/bill_002.pdf"
}),
serde_json::json!({
"id": "bill_003",
"date": "2024-03-01",
"description": "Monthly USD Credits Usage",
"amount": 520,
"status": "Paid",
"invoice_url": "/invoices/bill_003.pdf"
}),
serde_json::json!({
"id": "bill_004",
"date": "2024-02-01",
"description": "Monthly USD Credits Usage",
"amount": 290,
"status": "Paid",
"invoice_url": "/invoices/bill_004.pdf"
})
];
return Ok(ResponseBuilder::ok()
.json(serde_json::json!({
"success": true,
"billing_history": billing_history,
"total_bills": billing_history.len(),
"user_email": user.email
}))
.build()?);
}
Ok(ResponseBuilder::bad_request()
.json(serde_json::json!({
"success": false,
"message": "User not found"
}))
.build()?)
}
/// Get user's services
pub async fn get_user_services(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,
"error": "Pattern needs manual fix"
})).build();
}
};
// PHASE 1 FIX: Load services directly from persistent storage for consistency
let user_services = UserPersistence::get_user_services(&user_email);
let response = serde_json::json!({
"success": true,
"services": user_services
});
Ok(ResponseBuilder::ok()
.json(response)
.build()?)
}
/// Convert JSON Value to Service struct
fn json_to_service(json_value: &serde_json::Value) -> Option<crate::models::user::Service> {
// Handle missing required fields with defaults
let id = json_value.get("id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| {
// Generate a unique UUID if missing
Uuid::new_v4().to_string()
});
let name = json_value.get("name")
.and_then(|v| v.as_str())
.unwrap_or("Unnamed Service")
.to_string();
let category = json_value.get("category")
.and_then(|v| v.as_str())
.unwrap_or("General")
.to_string();
let description = json_value.get("description")
.and_then(|v| v.as_str())
.unwrap_or("No description provided")
.to_string();
let price_per_hour = json_value.get("price_per_hour")
.or_else(|| json_value.get("pricing"))
.or_else(|| json_value.get("price_amount"))
.or_else(|| json_value.get("hourly_rate"))
.and_then(|p| p.as_i64())
.unwrap_or(0) as i32;
let status = json_value.get("status")
.and_then(|v| v.as_str())
.unwrap_or("Active")
.to_string();
let clients = json_value.get("clients")
.and_then(|c| c.as_i64())
.unwrap_or(0) as i32;
let rating = json_value.get("rating")
.and_then(|r| r.as_f64())
.unwrap_or(0.0) as f32;
let total_hours = json_value.get("total_hours")
.and_then(|h| h.as_i64())
.unwrap_or(0) as i32;
Some(crate::models::user::Service {
id,
name,
category,
description,
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,
clients,
rating,
total_hours: Some(total_hours),
})
}
/// Create a new service
pub async fn create_service(
service_data: web::Json<serde_json::Value>,
session: Session,
) -> Result<impl Responder> {
// Convert JSON to Service struct
let service = match Self::json_to_service(&service_data) {
Some(service) => service,
None => {
let error_response = serde_json::json!({
"success": false,
"message": "Invalid service data format"
});
return Ok(ResponseBuilder::bad_request()
.json(error_response)
.build()?);
}
};
// Get user email from session for service persistence
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
let error_response = serde_json::json!({
"success": false,
"message": "User not authenticated"
});
return Ok(ResponseBuilder::unauthorized()
.json(error_response)
.build()?);
}
};
// Persist service to user's persistent data (this is the source of truth)
if let Err(e) = UserPersistence::add_user_service(&user_email, service.clone()) {
let error_response = serde_json::json!({
"success": false,
"message": "Failed to persist service data"
});
return Ok(ResponseBuilder::internal_error()
.json(error_response)
.build()?);
} else {
}
// Verify we can load the user to ensure they exist
if Self::load_user_with_persistent_data(&session).is_some() {
// Service registration is now handled through persistent user data
// Products are automatically aggregated by ProductService from user-owned data
// Prepare and send response
let response_json = serde_json::json!({
"success": true,
"message": "Service created successfully",
"service": {
"id": service.id,
"name": service.name,
"category": service.category,
"description": service.description,
"price_per_hour": service.price_per_hour_usd,
"status": service.status
}
});
return Ok(ResponseBuilder::ok()
.json(response_json)
.build()?);
}
let error_response = serde_json::json!({
"success": false,
"message": "Failed to create service - user not found or not a service provider"
});
Ok(ResponseBuilder::bad_request()
.json(error_response)
.build()?)
}
/// Update an existing service
pub async fn update_service(
path: web::Path<String>,
service_data: web::Json<serde_json::Value>,
session: Session,
) -> Result<impl Responder> {
let service_id = path.into_inner();
// Convert JSON to Service struct
let updated_service = match Self::json_to_service(&service_data) {
Some(mut service) => {
// Ensure the service ID matches the path parameter
service.id = service_id.clone();
service
},
None => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()?);
}
};
// 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,
"error": "Pattern needs manual fix"
})).build();
}
};
// Load user's persistent data
if let Some(mut persistent_data) = UserPersistence::load_user_data(&user_email) {
// Find and update the service in persistent storage
if let Some(service) = persistent_data.services.iter_mut().find(|s| s.id == service_id) {
*service = updated_service.clone();
// Save updated persistent data
if let Err(e) = UserPersistence::save_user_data(&persistent_data) {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()?);
}
return Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()?);
}
}
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()?)
}
/// Delete a service
pub async fn delete_service(
path: web::Path<String>,
session: Session,
) -> Result<impl Responder> {
let service_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,
"error": "Pattern needs manual fix"
})).build();
}
};
// Remove service from persistent storage
match UserPersistence::remove_user_service(&user_email, &service_id) {
Ok(true) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()?)
},
Ok(false) => {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()?)
},
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()?)
}
}
}
/// Get user service requests
pub async fn get_user_service_requests(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,
"error": "Pattern needs manual fix"
})).build();
}
};
// Load service requests from persistent storage
let user_requests = UserPersistence::get_user_service_requests(&user_email);
// New users start with empty service requests - no automatic mock data
let requests_to_return = user_requests;
let response = serde_json::json!({
"success": true,
"requests": requests_to_return
});
Ok(ResponseBuilder::ok()
.json(response)
.build()?)
}
/// Update service request status
pub async fn update_service_request(
path: web::Path<String>,
request_data: web::Json<serde_json::Value>,
session: Session,
) -> Result<impl Responder> {
let request_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"message": "User not authenticated"
})).build()?);
}
};
// Extract new status from request data
let new_status = request_data.get("status")
.and_then(|s| s.as_str())
.unwrap_or("In Progress");
let should_remove = request_data.get("remove")
.and_then(|r| r.as_bool())
.unwrap_or(false);
// If declining and should remove, remove the request entirely
if new_status == "Declined" && should_remove {
match UserPersistence::remove_user_service_request(&user_email, &request_id) {
Ok(true) => {
log::info!(
target: "dashboard_controller",
"update_service_request:removed provider_email={} request_id={}",
user_email, request_id
);
return Ok(ResponseBuilder::ok().json(serde_json::json!({
"message": "Service request declined and removed successfully",
"removed": true,
"request_id": request_id
})).build()?);
},
Ok(false) => {
return Ok(ResponseBuilder::not_found().json(serde_json::json!({
"message": "Service request not found",
"request_id": request_id
})).build()?);
},
Err(e) => {
log::error!(
target: "dashboard_controller",
"update_service_request:remove_error provider_email={} request_id={} error={}",
user_email, request_id, e
);
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"message": "Failed to remove service request",
"request_id": request_id
})).build()?);
}
}
}
// Regular status update
match UserPersistence::update_service_request_status(&user_email, &request_id, new_status) {
Ok(true) => {
// Load fresh data and find updated request for response and sync
let updated_requests = UserPersistence::get_user_service_requests(&user_email);
let updated_request = updated_requests
.iter()
.find(|r| r.id == request_id)
.cloned();
// Attempt to sync buyer booking if we have client email
let mut booking_sync = serde_json::json!({
"attempted": false,
"updated": false
});
if let Some(ref req) = updated_request {
let client_email = &req.customer_email;
let cd = req.completed_date.as_deref();
match UserPersistence::update_user_service_booking_fields(
client_email,
&request_id,
Some(new_status),
req.progress.map(|p| p as i32),
Some(req.priority.as_str()),
cd,
) {
Ok(updated) => {
booking_sync = serde_json::json!({
"attempted": true,
"updated": updated,
"buyer_email": client_email
});
}
Err(e) => {
log::error!(
target: "dashboard_controller",
"booking_sync_error provider_email={} buyer_email={} request_id={} error={}",
user_email, client_email, request_id, e
);
booking_sync = serde_json::json!({
"attempted": true,
"updated": false,
"buyer_email": client_email,
"error": "sync_failed"
});
}
}
}
log::info!(
target: "dashboard_controller",
"update_service_request:updated provider_email={} request_id={} status={} booking_sync_attempted={}",
user_email, request_id, new_status, booking_sync.get("attempted").and_then(|v| v.as_bool()).unwrap_or(false)
);
Ok(ResponseBuilder::ok().json(serde_json::json!({
"message": "Service request updated successfully",
"updated_request": updated_request,
"total_requests": updated_requests.len(),
"booking_sync": booking_sync
})).build()?)
},
Ok(false) => {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"message": "Service request not found",
"request_id": request_id
})).build()?)
},
Err(e) => {
log::error!(
target: "dashboard_controller",
"update_service_request:error provider_email={} request_id={} error={}",
user_email, request_id, e
);
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"message": "Failed to update service request",
"request_id": request_id
})).build()?)
}
}
}
/// Update service request progress with enhanced data
pub async fn update_service_request_progress(
path: web::Path<String>,
progress_data: web::Json<serde_json::Value>,
session: Session,
) -> Result<impl Responder> {
let request_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,
"error": "Pattern needs manual fix"
})).build();
}
};
// Extract progress data
let progress = progress_data.get("progress")
.and_then(|p| p.as_i64())
.unwrap_or(0) as i32;
let priority = progress_data.get("priority")
.and_then(|p| p.as_str())
.unwrap_or("Medium");
let hours_worked = progress_data.get("hours_worked")
.and_then(|h| h.as_f64())
.unwrap_or(0.0);
let notes = progress_data.get("notes")
.and_then(|n| n.as_str())
.unwrap_or("");
let notify_client = progress_data.get("notify_client")
.and_then(|n| n.as_bool())
.unwrap_or(false);
// Determine status based on progress
let new_status = progress_data.get("status")
.and_then(|s| s.as_str())
.unwrap_or(if progress >= 100 { "Completed" } else { "In Progress" });
// Update service request progress and status in persistent storage
match UserPersistence::update_service_request_progress(
&user_email,
&request_id,
progress,
Some(priority),
Some(hours_worked),
if notes.is_empty() { None } else { Some(notes) },
new_status
) {
Ok(true) => {
// Reload updated request to return and to drive sync
let updated_requests = UserPersistence::get_user_service_requests(&user_email);
let updated_request = updated_requests
.iter()
.find(|r| r.id == request_id)
.cloned();
// Attempt to sync buyer booking
let mut booking_sync = serde_json::json!({
"attempted": false,
"updated": false
});
if let Some(ref req) = updated_request {
let client_email = &req.customer_email;
let cd = req.completed_date.as_deref();
match UserPersistence::update_user_service_booking_fields(
client_email,
&request_id,
Some(new_status),
req.progress.map(|p| p as i32),
Some(req.priority.as_str()),
cd,
) {
Ok(updated) => {
booking_sync = serde_json::json!({
"attempted": true,
"updated": updated,
"buyer_email": client_email
});
}
Err(e) => {
log::error!(
target: "dashboard_controller",
"booking_sync_error provider_email={} buyer_email={} request_id={} error={}",
user_email, client_email, request_id, e
);
booking_sync = serde_json::json!({
"attempted": true,
"updated": false,
"buyer_email": client_email,
"error": "sync_failed"
});
}
}
}
log::info!(
target: "dashboard_controller",
"update_service_request_progress:updated provider_email={} request_id={} status={} progress={} notify_client={} booking_sync_attempted={}",
user_email, request_id, new_status, progress, notify_client,
booking_sync.get("attempted").and_then(|v| v.as_bool()).unwrap_or(false)
);
Ok(ResponseBuilder::ok().json(serde_json::json!({
"message": "Service request progress updated successfully",
"updated_request": updated_request,
"total_requests": updated_requests.len(),
"booking_sync": booking_sync
})).build()?)
},
Ok(false) => {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"message": "Service request not found",
"request_id": request_id
})).build()?)
},
Err(e) => {
log::error!(
target: "dashboard_controller",
"update_service_request_progress:error provider_email={} request_id={} error={}",
user_email, request_id, e
);
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"message": "Failed to update service request progress",
"request_id": request_id
})).build()?)
}
}
}
/// Get user availability settings
pub async fn get_user_availability(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,
"error": "Pattern needs manual fix"
})).build();
}
};
// Load availability from persistence
let availability = UserPersistence::get_user_availability(&user_email)
.unwrap_or_else(|| {
// Default availability settings
crate::services::user_persistence::AvailabilitySettings {
available: true,
weekly_hours: 20,
updated_at: chrono::Utc::now().to_rfc3339(),
}
});
ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()
}
/// Update user availability settings
pub async fn update_user_availability(
availability_data: web::Json<serde_json::Value>,
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,
"error": "Pattern needs manual fix"
})).build();
}
};
// Parse availability data
let available = availability_data.get("available")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let weekly_hours = availability_data.get("weekly_hours")
.and_then(|v| v.as_i64())
.unwrap_or(20) as i32;
let availability_settings = crate::services::user_persistence::AvailabilitySettings {
available,
weekly_hours,
updated_at: chrono::Utc::now().to_rfc3339(),
};
// Save to persistence
if let Err(e) = UserPersistence::update_user_availability(&user_email, availability_settings) {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build();
}
ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()
}
/// PHASE 3 FIX: Create SLA with full implementation
pub async fn create_sla(
sla_data: web::Json<serde_json::Value>,
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,
"error": "Pattern needs manual fix"
})).build();
}
};
// Extract SLA data from JSON
let sla_id = sla_data.get("id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| format!("sla_{}", Utc::now().timestamp()));
let name = sla_data.get("name")
.and_then(|v| v.as_str())
.unwrap_or("Unnamed SLA")
.to_string();
let description = sla_data.get("description")
.and_then(|v| v.as_str())
.unwrap_or("No description provided")
.to_string();
let service_id = sla_data.get("service_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let response_time_hours = sla_data.get("response_time_hours")
.and_then(|v| v.as_i64())
.unwrap_or(24) as i32;
let resolution_time_hours = sla_data.get("resolution_time_hours")
.and_then(|v| v.as_i64())
.unwrap_or(72) as i32;
let availability_percentage = sla_data.get("availability_percentage")
.and_then(|v| v.as_f64())
.unwrap_or(99.9) as f32;
let support_hours = sla_data.get("support_hours")
.and_then(|v| v.as_str())
.unwrap_or("Business Hours")
.to_string();
let escalation_procedure = sla_data.get("escalation_procedure")
.and_then(|v| v.as_str())
.unwrap_or("Standard escalation procedure")
.to_string();
// Create SLA struct
let sla = crate::services::user_persistence::ServiceLevelAgreement {
id: sla_id.clone(),
name: name.clone(),
description,
service_id,
response_time_hours,
resolution_time_hours,
availability_percentage,
support_hours,
escalation_procedure,
penalties: Vec::new(), // Can be enhanced later
created_at: Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
status: "Active".to_string(),
};
// Save SLA to persistent storage
match UserPersistence::add_user_sla(&user_email, sla.clone()) {
Ok(()) => {
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "SLA created successfully",
"sla": sla
})).build()
}
Err(e) => {
ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Failed to persist SLA",
"error": e.to_string()
})).build()
}
}
}
/// PHASE 3 FIX: Get user's SLAs
pub async fn get_user_slas(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,
"error": "Pattern needs manual fix"
})).build();
}
};
// Load SLAs from persistent storage
let user_slas = UserPersistence::get_user_slas(&user_email);
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"slas": user_slas
})).build()
}
/// PHASE 3 FIX: Update SLA
pub async fn update_sla(
path: web::Path<String>,
sla_data: web::Json<serde_json::Value>,
session: Session,
) -> Result<impl Responder> {
let sla_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()?);
}
};
// Get existing SLA
let mut existing_sla = match UserPersistence::get_user_sla_by_id(&user_email, &sla_id) {
Some(sla) => sla,
None => {
return ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build();
}
};
// Update fields if provided
if let Some(name) = sla_data.get("name").and_then(|v| v.as_str()) {
existing_sla.name = name.to_string();
}
if let Some(description) = sla_data.get("description").and_then(|v| v.as_str()) {
existing_sla.description = description.to_string();
}
if let Some(response_time) = sla_data.get("response_time_hours").and_then(|v| v.as_i64()) {
existing_sla.response_time_hours = response_time as i32;
}
if let Some(resolution_time) = sla_data.get("resolution_time_hours").and_then(|v| v.as_i64()) {
existing_sla.resolution_time_hours = resolution_time as i32;
}
if let Some(availability) = sla_data.get("availability_percentage").and_then(|v| v.as_f64()) {
existing_sla.availability_percentage = availability as f32;
}
if let Some(status) = sla_data.get("status").and_then(|v| v.as_str()) {
existing_sla.status = status.to_string();
}
// Save updated SLA
match UserPersistence::update_user_sla(&user_email, existing_sla.clone()) {
Ok(true) => {
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "SLA updated successfully",
"sla": existing_sla
})).build()
}
Ok(false) => {
ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"message": "SLA not found"
})).build()
}
Err(e) => {
ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Failed to update SLA",
"error": e.to_string()
})).build()
}
}
}
/// PHASE 3 FIX: Delete SLA
pub async fn delete_sla(
path: web::Path<String>,
session: Session,
) -> Result<impl Responder> {
let sla_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,
"error": "Pattern needs manual fix"
})).build();
}
};
// Remove SLA
match UserPersistence::remove_user_sla(&user_email, &sla_id) {
Ok(true) => {
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "SLA deleted successfully",
"id": sla_id
})).build()
}
Ok(false) => {
ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"message": "SLA not found"
})).build()
}
Err(e) => {
ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Failed to delete SLA",
"error": e.to_string()
})).build()
}
}
}
/// Download service provider agreement
pub async fn download_agreement(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,
"error": "Pattern needs manual fix"
})).build();
}
};
// Generate text agreement content
let agreement_content = format!(
"SERVICE PROVIDER AGREEMENT\n\n\
ThreeFold Grid Service Provider Agreement\n\
\n\
Service Provider: {}\n\
Agreement Date: January 15, 2025\n\
Renewal Date: January 15, 2026\n\
Status: Active\n\
\n\
This agreement outlines the terms and conditions for providing services\n\
on the ThreeFold Grid platform.\n\
\n\
Terms:\n\
1. Service Quality Standards\n\
2. Payment Terms and Conditions\n\
3. Data Protection and Privacy\n\
4. Intellectual Property Rights\n\
5. Termination Conditions\n\
\n\
For full terms and conditions, please visit:\n\
https://threefold.io/terms/service-providers\n\
\n\
Generated on: {}\n",
user_email,
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
);
// Return the agreement as a downloadable text file
ResponseBuilder::ok()
.content_type("text/plain")
.add_metadata("Content-Disposition", format!("attachment; filename=\"service-provider-agreement-{}.txt\"", user_email.replace("@", "_at_")))
.body(agreement_content)
.build()
}
/// Get detailed service request information
pub async fn get_service_request_details(
path: web::Path<String>,
session: Session,
) -> Result<impl Responder> {
let request_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,
"error": "Pattern needs manual fix"
})).build();
}
};
// Get user service requests
let service_requests = UserPersistence::get_user_service_requests(&user_email);
// Find the specific request
if let Some(request) = service_requests.iter().find(|r| r.id == request_id) {
// Create detailed request object with additional fields
let detailed_request = serde_json::json!({
"id": request.id,
"client_name": request.customer_email.split('@').next().unwrap_or(&request.customer_email),
"service_name": request.service_name,
"status": request.status,
"requested_date": request.requested_date,
"estimated_hours": request.estimated_hours,
"budget": request.budget,
"priority": request.priority,
"progress": request.progress.unwrap_or(if request.status == "Completed" { 100.0 } else if request.status == "In Progress" { 50.0 } else { 0.0 }),
"description": format!("Service request for {} from {}", request.service_name, request.customer_email.split('@').next().unwrap_or(&request.customer_email)),
"notes": "Progress updates and notes will appear here."
});
ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()
} else {
ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()
}
}
/// Get completed request details with additional metrics
pub async fn get_completed_request_details(
path: web::Path<String>,
session: Session,
) -> Result<impl Responder> {
let request_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Get user service requests
let service_requests = UserPersistence::get_user_service_requests(&user_email);
// Find the specific completed request
if let Some(request) = service_requests.iter().find(|r| r.id == request_id && r.status == "Completed") {
// Create detailed completed request object with metrics
let completed_request = serde_json::json!({
"id": request.id,
"client_name": request.customer_email.split('@').next().unwrap_or(&request.customer_email),
"service_name": request.service_name,
"completed_date": chrono::Utc::now().format("%Y-%m-%d").to_string(),
"hours_logged": request.estimated_hours,
"revenue": request.budget,
"rating": 4.8,
"on_time": true,
"summary": format!("Successfully completed {} service for {}. All requirements met and client satisfied.", request.service_name, request.customer_email.split('@').next().unwrap_or(&request.customer_email)),
"client_feedback": "Excellent work! Professional, timely, and exceeded expectations."
});
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
} else {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
/// Generate invoice for completed request (HTML page for viewing and printing)
pub async fn generate_service_request_invoice(
tmpl: web::Data<Tera>,
path: web::Path<String>,
session: Session,
) -> Result<impl Responder> {
let request_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Load user with mock data
if let Some(user) = Self::load_user_with_persistent_data(&session) {
// Get user service requests
let service_requests = UserPersistence::get_user_service_requests(&user_email);
// Find the specific completed request
if let Some(request) = service_requests.iter().find(|r| r.id == request_id && r.status == "Completed") {
// Calculate rate safely
let rate = if let Some(hours) = request.estimated_hours {
if hours > 0 {
request.budget / rust_decimal::Decimal::from(hours)
} else {
rust_decimal::Decimal::ZERO
}
} else {
rust_decimal::Decimal::ZERO
};
// Generate dates
let invoice_date = chrono::Utc::now().format("%Y-%m-%d").to_string();
let due_date = (chrono::Utc::now() + chrono::Duration::days(30)).format("%Y-%m-%d").to_string();
let generated_date = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string();
// Format status for CSS class (lowercase, replace spaces with dashes)
let status_class = request.status.to_lowercase().replace(' ', "-");
let mut ctx = tera::Context::new();
ctx.insert("active_page", "dashboard");
ctx.insert("active_section", "service-provider");
ctx.insert("request", request);
ctx.insert("status_class", &status_class);
ctx.insert("rate", &rate);
ctx.insert("user", &user);
ctx.insert("invoice_date", &invoice_date);
ctx.insert("due_date", &due_date);
ctx.insert("generated_date", &generated_date);
Ok(render_template(&tmpl, "dashboard/service_request_invoice.html", &ctx))
} else {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
} else {
Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
/// Get service request report page (for viewing and printing)
pub async fn get_service_request_report(
tmpl: web::Data<Tera>,
path: web::Path<String>,
session: Session,
) -> Result<impl Responder> {
let request_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Load user with mock data
if let Some(user) = Self::load_user_with_persistent_data(&session) {
// Load persistent data for this user
if let Some(persistent_data) = UserPersistence::load_user_data(&user.email) {
// Find the completed request from service_requests
if let Some(request) = persistent_data.service_requests.iter()
.find(|r| r.id == request_id && r.status == "Completed") {
// Calculate rate (budget / estimated_hours)
let rate = if let Some(hours) = request.estimated_hours {
if hours > 0 {
request.budget / rust_decimal::Decimal::from(hours)
} else {
rust_decimal::Decimal::ZERO
}
} else {
rust_decimal::Decimal::ZERO
};
// Format status for CSS class (lowercase, replace spaces with dashes)
let status_class = request.status.to_lowercase().replace(' ', "-");
let mut ctx = tera::Context::new();
ctx.insert("active_page", "dashboard");
ctx.insert("active_section", "service-provider");
ctx.insert("request", request);
ctx.insert("status_class", &status_class);
ctx.insert("rate", &rate);
ctx.insert("user", &user);
Ok(render_template(&tmpl, "dashboard/service_request_report.html", &ctx))
} else {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Service request not found or not completed"
})).build())
}
} else {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Failed to load user data"
})).build())
}
} else {
Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "User session not found"
})).build())
}
}
/// Get comprehensive service details with analytics and client data
pub async fn get_service_details(
path: web::Path<String>,
session: Session,
) -> Result<impl Responder> {
let service_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Load user's services from persistent storage
let user_services = UserPersistence::get_user_services(&user_email);
// Find the specific service
if let Some(service) = user_services.iter().find(|s| s.id == service_id) {
// Load service requests to calculate analytics
let service_requests = UserPersistence::get_user_service_requests(&user_email);
let service_related_requests: Vec<_> = service_requests.iter()
.filter(|req| req.service_name == service.name)
.collect();
// Calculate analytics
let total_revenue: i32 = service_related_requests.iter()
.filter(|req| req.status == "Completed")
.map(|req| req.budget.to_string().parse::<i32>().unwrap_or(0))
.sum();
let completed_requests = service_related_requests.iter()
.filter(|req| req.status == "Completed")
.count();
let avg_rating = if completed_requests > 0 { service.rating } else { 0.0 };
let on_time_delivery = if completed_requests > 0 { 95.0 } else { 0.0 }; // Mock calculation
// Generate monthly revenue data (mock for now)
let monthly_revenue = if total_revenue > 0 { total_revenue / 3 } else { 0 };
// Get active clients (unique client names from non-completed requests)
let active_clients: std::collections::HashSet<_> = service_related_requests.iter()
.filter(|req| req.status != "Completed")
.map(|req| req.customer_email.split('@').next().unwrap_or(&req.customer_email).to_string())
.collect();
let response = serde_json::json!({
"success": true,
"service": {
"id": service.id,
"name": service.name,
"description": service.description,
"category": service.category,
"price_per_hour": service.price_per_hour_usd,
"status": service.status,
"clients": service.clients,
"rating": service.rating,
"total_hours": service.total_hours,
"analytics": {
"revenue": {
"total": total_revenue,
"monthly": monthly_revenue,
"trend": [monthly_revenue - 2000, monthly_revenue - 1000, monthly_revenue]
},
"performance": {
"avg_rating": avg_rating,
"on_time_delivery": on_time_delivery,
"response_time_avg": 1.5
},
"client_satisfaction": avg_rating,
"completed_projects": completed_requests,
"active_clients": active_clients.len()
},
"availability": {
"weekly_hours": 20,
"response_time": "2 hours"
},
"skills": ["System Administration", "Security", "DevOps"], // Mock data
"experience_level": "Expert"
}
});
Ok(ResponseBuilder::ok().json(response).build())
} else {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
/// Get service analytics data
pub async fn get_service_analytics(
path: web::Path<String>,
session: Session,
) -> Result<impl Responder> {
let service_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Load user's services and requests
let user_services = UserPersistence::get_user_services(&user_email);
let service_requests = UserPersistence::get_user_service_requests(&user_email);
if let Some(service) = user_services.iter().find(|s| s.id == service_id) {
let service_related_requests: Vec<_> = service_requests.iter()
.filter(|req| req.service_name == service.name)
.collect();
// Calculate detailed analytics
let total_revenue: i32 = service_related_requests.iter()
.filter(|req| req.status == "Completed")
.map(|req| req.budget.to_string().parse::<i32>().unwrap_or(0))
.sum();
let total_hours: i32 = service_related_requests.iter()
.filter(|req| req.status == "Completed")
.map(|req| req.estimated_hours.unwrap_or(0))
.sum();
let completed_count = service_related_requests.iter()
.filter(|req| req.status == "Completed")
.count();
// Generate revenue trend (last 6 months)
let mut revenue_trend = Vec::new();
let monthly_avg = if total_revenue > 0 { total_revenue / 6 } else { 0 };
for i in 0..6 {
let variation = (i as f32 * 0.2 + 0.8) * monthly_avg as f32;
revenue_trend.push(variation as i32);
}
let response = serde_json::json!({
"success": true,
"analytics": {
"revenue": {
"total": total_revenue,
"monthly": monthly_avg,
"trend": revenue_trend,
"per_hour_avg": if total_hours > 0 { total_revenue / total_hours } else { 0 }
},
"performance": {
"avg_rating": service.rating,
"on_time_delivery": 95.0,
"response_time_avg": 1.5,
"completion_rate": if service_related_requests.len() > 0 {
(completed_count as f32 / service_related_requests.len() as f32) * 100.0
} else { 0.0 }
},
"client_metrics": {
"total_clients": service.clients,
"repeat_clients": (service.clients as f32 * 0.7) as i32,
"client_satisfaction": service.rating,
"referral_rate": 25.0
},
"project_metrics": {
"total_projects": service_related_requests.len(),
"completed_projects": completed_count,
"total_hours": total_hours,
"avg_project_size": if completed_count > 0 { total_hours / completed_count as i32 } else { 0 }
}
}
});
Ok(ResponseBuilder::ok().json(response).build())
} else {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
/// Get service client history and relationships
pub async fn get_service_clients(
path: web::Path<String>,
session: Session,
) -> Result<impl Responder> {
let service_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Load user's services and requests
let user_services = UserPersistence::get_user_services(&user_email);
let service_requests = UserPersistence::get_user_service_requests(&user_email);
if let Some(service) = user_services.iter().find(|s| s.id == service_id) {
let service_related_requests: Vec<_> = service_requests.iter()
.filter(|req| req.service_name == service.name)
.collect();
// Group requests by client
let mut client_data = std::collections::HashMap::new();
for request in &service_related_requests {
let client_entry = client_data.entry(request.customer_email.split('@').next().unwrap_or(&request.customer_email).to_string()).or_insert_with(|| {
serde_json::json!({
"name": request.customer_email.split('@').next().unwrap_or(&request.customer_email),
"email": request.customer_email.clone(),
"phone": "", // No phone field in ServiceRequest
"projects": [],
"total_revenue": 0,
"total_hours": 0,
"avg_rating": 0.0,
"status": "Active"
})
});
// Add project to client
if let Some(projects) = client_entry.get_mut("projects") {
if let Some(projects_array) = projects.as_array_mut() {
projects_array.push(serde_json::json!({
"id": request.id,
"service_name": request.service_name,
"status": request.status,
"requested_date": request.requested_date,
"completed_date": request.completed_date,
"budget": request.budget,
"estimated_hours": request.estimated_hours,
"priority": request.priority,
"description": request.description.clone().unwrap_or_default()
}));
}
}
// Update totals
if let Some(total_revenue) = client_entry.get_mut("total_revenue") {
if request.status == "Completed" {
*total_revenue = serde_json::Value::Number(
serde_json::Number::from(total_revenue.as_i64().unwrap_or(0) + request.budget.to_i64().unwrap_or(0))
);
}
}
if let Some(total_hours) = client_entry.get_mut("total_hours") {
if request.status == "Completed" {
*total_hours = serde_json::Value::Number(
serde_json::Number::from(total_hours.as_i64().unwrap_or(0) + request.estimated_hours.unwrap_or(0) as i64)
);
}
}
}
// Convert to array and add ratings
let clients: Vec<_> = client_data.into_values().map(|mut client| {
// Set average rating (mock calculation)
let rating = 4.2 + (rand::random::<f64>() * 1.6);
client["avg_rating"] = serde_json::Value::Number(
serde_json::Number::from_f64(rating).unwrap_or_else(|| serde_json::Number::from(4))
);
client
}).collect();
let response = serde_json::json!({
"success": true,
"clients": clients,
"summary": {
"total_clients": clients.len(),
"active_clients": clients.iter().filter(|c| c["status"] == "Active").count(),
"total_projects": service_related_requests.len(),
"completed_projects": service_related_requests.iter().filter(|r| r.status == "Completed").count()
}
});
Ok(ResponseBuilder::ok().json(response).build())
} else {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
/// Update service status (Active/Paused/Draft)
pub async fn update_service_status(
path: web::Path<String>,
status_data: web::Json<serde_json::Value>,
session: Session,
) -> Result<impl Responder> {
let service_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Extract new status
let new_status = status_data.get("status")
.and_then(|s| s.as_str())
.unwrap_or("Active")
.to_string();
// Validate status
if !["Active", "Paused", "Draft"].contains(&new_status.as_str()) {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
// Load user's persistent data
if let Some(mut persistent_data) = UserPersistence::load_user_data(&user_email) {
// Find and update the service status
if let Some(service) = persistent_data.services.iter_mut().find(|s| s.id == service_id) {
let old_status = service.status.clone();
let service_name = service.name.clone();
let service_id_clone = service.id.clone();
service.status = new_status.clone();
// Save updated persistent data
if let Err(e) = UserPersistence::save_user_data(&persistent_data) {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
return Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
}
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
// ========================================
// APP PROVIDER MANAGEMENT ENDPOINTS
// ========================================
/// Get all apps for the current user
pub async fn get_user_apps(session: Session) -> Result<impl Responder> {
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
Ok(None) => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build();
},
Err(e) => {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build();
}
};
let apps = UserPersistence::get_user_apps(&user_email);
ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()
}
/// Create a new app
pub async fn create_app(
app_data: web::Json<serde_json::Value>,
session: Session
) -> Result<impl Responder> {
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
Ok(None) => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build();
},
Err(e) => {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build();
}
};
// Convert JSON to PublishedApp
let app = match Self::json_to_app(&app_data) {
Some(app) => app,
None => {
return ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build();
}
};
// Save the app
match UserPersistence::add_user_app(&user_email, app.clone()) {
Ok(()) => {
// App registration is now handled through persistent user data
// Products are automatically aggregated by ProductService from user-owned data
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": "App published successfully"
})).build()
},
Err(e) => {
ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": format!("Failed to publish app: {}", e)
})).build()
}
}
}
/// Update an existing app
pub async fn update_app(
app_id: web::Path<String>,
app_data: web::Json<serde_json::Value>,
session: Session
) -> Result<impl Responder> {
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
Ok(None) => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build();
},
Err(e) => {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build();
}
};
// Convert JSON to PublishedApp
let mut updated_app = match Self::json_to_app(&app_data) {
Some(app) => app,
None => {
return ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build();
}
};
// Ensure the app ID matches
updated_app.id = app_id.to_string();
// Update the app
match UserPersistence::update_user_app(&user_email, updated_app.clone()) {
Ok(true) => {
ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()
},
Ok(false) => {
ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()
},
Err(e) => {
ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()
}
}
}
/// Delete an app
pub async fn delete_app(
app_id: web::Path<String>,
session: Session
) -> Result<impl Responder> {
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
Ok(None) => {
return ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build();
},
Err(e) => {
return ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build();
}
};
// Delete the app
match UserPersistence::remove_user_app(&user_email, &app_id) {
Ok(true) => {
ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()
},
Ok(false) => {
ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()
},
Err(e) => {
ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build()
}
}
}
/// API endpoint to get deployment details by ID
pub async fn get_deployment_details(
path: web::Path<String>,
session: Session
) -> Result<impl Responder> {
let deployment_id = path.into_inner();
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
Ok(None) => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"message": "Unauthorized"
})).build());
},
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"message": "Failed to retrieve session"
})).build());
}
};
// Load user's app deployments
let deployments = UserPersistence::get_user_application_deployments(&user_email);
// Find the specific deployment
if let Some(deployment) = deployments.iter().find(|d| d.id == deployment_id) {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"deployment": deployment
})).build())
} else {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"message": "Deployment not found"
})).build())
}
}
/// Helper function to convert JSON to PublishedApp
fn json_to_app(json_value: &serde_json::Value) -> Option<crate::models::user::PublishedApp> {
let name = json_value.get("name")?.as_str()?.to_string();
let category = json_value.get("category")?.as_str().unwrap_or("Other").to_string();
let version = json_value.get("version")?.as_str().unwrap_or("1.0.0").to_string();
let status = json_value.get("status")?.as_str().unwrap_or("Active").to_string();
let deployments = json_value.get("deployments")?.as_i64().unwrap_or(0) as i32;
let rating = json_value.get("rating")?.as_f64().unwrap_or(4.5) as f32;
let monthly_revenue_usd = json_value
.get("monthly_revenue_usd")
.or_else(|| json_value.get("monthly_revenue"))
.and_then(|v| v.as_i64())
.unwrap_or(0) as i32;
let last_updated = json_value.get("last_updated")?.as_str().unwrap_or("Today").to_string();
// Generate ID if not provided
let id = json_value.get("id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| format!("app_{}", Utc::now().timestamp()));
Some(crate::models::user::PublishedApp::builder()
.id(id)
.name(name)
.category(category)
.version(version)
.status(status)
.deployments(deployments)
.rating(rating)
.monthly_revenue_usd(monthly_revenue_usd)
.last_updated(last_updated)
.auto_healing(true) // Default to enabled for new apps
.build()
.unwrap())
}
/// Delete a node
pub async fn delete_node(
session: Session,
path: web::Path<String>,
) -> Result<impl Responder> {
let node_id = path.into_inner();
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
Ok(None) => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Check if node exists and get its details first
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Verify node exists and belongs to user
let _node = match resource_provider_service.get_node_by_id(&user_email, &node_id) {
Some(node) => node,
None => {
return Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Check if node has active rentals (optional safety check)
// This could be expanded to check for active slice rentals
// Remove node from persistent storage
match UserPersistence::remove_user_node(&user_email, &node_id) {
Ok(_) => {
// TODO: Remove associated marketplace products
// This would involve removing slice products and full node products
// from the marketplace that were created for this node
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to delete node",
"details": e.to_string()
})).build())
}
}
}
/// Update node configuration (group, slice formats, rental options)
pub async fn update_node_configuration(
session: Session,
path: web::Path<String>,
form: web::Json<serde_json::Value>,
) -> Result<impl Responder> {
let node_id = path.into_inner();
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
Ok(None) => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
let _resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Parse the configuration update
let config_data = form.into_inner();
// Extract configuration fields
let node_group_id = config_data.get("node_group_id")
.and_then(|v| v.as_str())
.map(|s| if s.is_empty() { None } else { Some(s.to_string()) })
.flatten();
let slice_formats = config_data.get("slice_formats")
.and_then(|v| v.as_array())
.map(|arr| {
let mut formats: Vec<String> = arr.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect();
// Remove duplicates while preserving order
formats.dedup();
formats
});
let full_node_rental_enabled = config_data.get("full_node_rental_enabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
// Parse comprehensive full node pricing configuration
let full_node_pricing = if let Some(pricing_data) = config_data.get("full_node_pricing") {
if let Some(pricing_obj) = pricing_data.as_object() {
let hourly = pricing_obj.get("hourly")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let daily = pricing_obj.get("daily")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let monthly = pricing_obj.get("monthly")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let yearly = pricing_obj.get("yearly")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let auto_calculate = pricing_obj.get("auto_calculate")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let daily_discount_percent = pricing_obj.get("daily_discount_percent")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let monthly_discount_percent = pricing_obj.get("monthly_discount_percent")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let yearly_discount_percent = pricing_obj.get("yearly_discount_percent")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
// Create pricing using builder pattern
let setup_fee = pricing_obj.get("setup_fee")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or_default());
let deposit_required = pricing_obj.get("deposit_required")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or_default());
let full_node_pricing = crate::models::user::FullNodePricing {
monthly_cost: monthly,
setup_fee,
deposit_required,
..Default::default()
};
Some(full_node_pricing)
} else {
None
}
} else {
None
};
// Legacy support for old monthly price field
let full_node_monthly_price = config_data.get("full_node_monthly_price")
.and_then(|v| v.as_f64())
.map(|price| rust_decimal::Decimal::try_from(price).unwrap_or(rust_decimal::Decimal::ZERO));
let minimum_rental_days = config_data.get("minimum_rental_days")
.and_then(|v| v.as_u64())
.unwrap_or(30) as u32;
let maximum_rental_days = config_data.get("maximum_rental_days")
.and_then(|v| v.as_u64())
.unwrap_or(365) as u32;
let auto_renewal_enabled = config_data.get("auto_renewal_enabled")
.and_then(|v| v.as_bool());
let availability_status = config_data.get("availability_status")
.and_then(|v| v.as_str())
.and_then(|s| match s {
"Available" => Some(crate::models::user::NodeAvailabilityStatus::Available),
"PartiallyRented" | "FullyRented" | "Rented" => Some(crate::models::user::NodeAvailabilityStatus::Rented),
"Unavailable" | "Offline" => Some(crate::models::user::NodeAvailabilityStatus::Offline),
"Maintenance" => Some(crate::models::user::NodeAvailabilityStatus::Maintenance),
_ => None,
});
// Only validate slice formats if they are being updated
if let Some(ref formats) = slice_formats {
if formats.is_empty() && !full_node_rental_enabled {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
}
// Load current node data
let mut persistent_data = match UserPersistence::load_user_data(&user_email) {
Some(data) => data,
None => {
return Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Find and update the node
if let Some(node) = persistent_data.nodes.iter_mut().find(|n| n.id == node_id) {
// Update node group only if provided
if config_data.get("node_group_id").is_some() {
if let Some(group_id) = node_group_id {
// Node group assignment - store in grid_data or rental_options
if let Some(rental_options) = &mut node.rental_options {
if let Some(rental_obj) = rental_options.as_object_mut() {
rental_obj.insert("node_group_id".to_string(), serde_json::Value::String(group_id));
}
}
} else {
// Remove node group assignment
if let Some(rental_options) = &mut node.rental_options {
if let Some(rental_obj) = rental_options.as_object_mut() {
rental_obj.remove("node_group_id");
}
}
}
}
// Update slice formats only if provided
if config_data.get("slice_formats").is_some() {
if let Some(ref formats) = slice_formats {
node.slice_formats = Some(formats.clone());
}
}
// Update rental options only if provided
if config_data.get("full_node_rental_enabled").is_some() ||
config_data.get("full_node_pricing").is_some() ||
config_data.get("full_node_monthly_price").is_some() ||
config_data.get("minimum_rental_days").is_some() {
if let Some(ref mut rental_options) = node.rental_options {
// Update existing rental options
if config_data.get("full_node_rental_enabled").is_some() {
if let Some(rental_obj) = rental_options.as_object_mut() {
rental_obj.insert("full_node_rental_enabled".to_string(), serde_json::Value::Bool(full_node_rental_enabled));
}
}
// Update with new comprehensive pricing if provided
if let Some(pricing) = full_node_pricing {
if let Some(rental_obj) = rental_options.as_object_mut() {
rental_obj.insert("full_node_pricing".to_string(), serde_json::to_value(pricing).unwrap_or_default());
}
} else if let Some(price) = full_node_monthly_price {
// Legacy support: convert monthly price to comprehensive pricing
let legacy_pricing = crate::models::user::FullNodePricing {
monthly_cost: price,
setup_fee: None,
deposit_required: None,
..Default::default()
};
if let Some(rental_obj) = rental_options.as_object_mut() {
rental_obj.insert("full_node_pricing".to_string(), serde_json::to_value(legacy_pricing).unwrap_or_default());
}
}
if config_data.get("minimum_rental_days").is_some() {
if let Some(rental_obj) = rental_options.as_object_mut() {
rental_obj.insert("minimum_rental_days".to_string(), serde_json::Value::Number(serde_json::Number::from(minimum_rental_days)));
}
}
} else if full_node_rental_enabled || full_node_pricing.is_some() {
// Create rental options directly
let pricing = if let Some(pricing) = full_node_pricing {
pricing
} else if let Some(price) = full_node_monthly_price {
// Legacy support: convert monthly price to comprehensive pricing
crate::models::user::FullNodePricing {
monthly_cost: price,
setup_fee: None,
deposit_required: None,
..Default::default()
}
} else {
crate::models::user::FullNodePricing {
monthly_cost: rust_decimal::Decimal::ZERO,
setup_fee: None,
deposit_required: None,
..Default::default()
}
};
let rental_options = crate::models::user::NodeRentalOptions {
full_node_available: full_node_rental_enabled,
slice_formats: slice_formats.unwrap_or_default(),
pricing: pricing.clone(),
slice_rental_enabled: true,
minimum_rental_days: if minimum_rental_days == 0 { 1 } else { minimum_rental_days },
maximum_rental_days: Some(maximum_rental_days),
full_node_rental_enabled: full_node_rental_enabled,
full_node_pricing: if full_node_rental_enabled { Some(pricing) } else { None },
auto_renewal_enabled: auto_renewal_enabled.unwrap_or(true),
};
node.rental_options = Some(serde_json::to_value(rental_options).unwrap_or_default());
}
}
// Update availability status only if provided
if config_data.get("availability_status").is_some() {
if let Some(status) = availability_status {
// Store availability status in grid_data or rental_options
if let Some(grid_data) = &mut node.grid_data {
if let Some(grid_obj) = grid_data.as_object_mut() {
grid_obj.insert("availability_status".to_string(), serde_json::to_value(status).unwrap_or_default());
}
} else {
let mut grid_data = serde_json::Map::new();
grid_data.insert("availability_status".to_string(), serde_json::to_value(status).unwrap_or_default());
node.grid_data = Some(serde_json::Value::Object(grid_data));
}
}
}
// Update staking configuration if provided
if config_data.get("staking_enabled").is_some() ||
config_data.get("staked_amount").is_some() ||
config_data.get("staking_period_months").is_some() ||
config_data.get("early_withdrawal_allowed").is_some() ||
config_data.get("early_withdrawal_penalty_percent").is_some() {
let staking_enabled = config_data.get("staking_enabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if staking_enabled {
// Parse staking configuration
let staked_amount = config_data.get("staked_amount")
.and_then(|v| v.as_f64())
.map(|amount| rust_decimal::Decimal::try_from(amount).unwrap_or(rust_decimal::Decimal::ZERO))
.unwrap_or(rust_decimal::Decimal::ZERO);
let staking_period_months = config_data.get("staking_period_months")
.and_then(|v| v.as_u64())
.unwrap_or(12) as u32;
let early_withdrawal_allowed = config_data.get("early_withdrawal_allowed")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let early_withdrawal_penalty_percent = config_data.get("early_withdrawal_penalty_percent")
.and_then(|v| v.as_f64())
.unwrap_or(25.0) as f32;
// Validate wallet balance if this is a new staking or increase
let current_staked = if let Some(grid_data) = &node.grid_data {
grid_data.get("staked_amount")
.and_then(|v| v.as_str())
.and_then(|s| rust_decimal::Decimal::from_str(s).ok())
.unwrap_or(rust_decimal::Decimal::ZERO)
} else {
rust_decimal::Decimal::ZERO
};
let additional_stake_needed = staked_amount - current_staked;
if additional_stake_needed > Decimal::ZERO {
if persistent_data.wallet_balance_usd < additional_stake_needed {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
// Deduct additional stake from wallet
persistent_data.wallet_balance_usd -= additional_stake_needed;
} else if additional_stake_needed < Decimal::ZERO {
// Return excess stake to wallet (with potential penalty)
let return_amount = additional_stake_needed.abs();
let penalty = if let Some(grid_data) = &node.grid_data {
let early_withdrawal_allowed = grid_data.get("early_withdrawal_allowed")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let penalty_percent = grid_data.get("early_withdrawal_penalty_percent")
.and_then(|v| v.as_f64())
.unwrap_or(25.0);
if early_withdrawal_allowed {
return_amount * rust_decimal::Decimal::try_from(penalty_percent).unwrap_or_default() / rust_decimal::Decimal::from(100)
} else {
rust_decimal::Decimal::ZERO
}
} else {
rust_decimal::Decimal::ZERO
};
persistent_data.wallet_balance_usd += return_amount - penalty;
}
// Build staking options
match NodeStakingOptionsBuilder::new()
.staking_enabled(true)
.staked_amount(staked_amount)
.staking_period_months(staking_period_months)
.early_withdrawal_allowed(early_withdrawal_allowed)
.early_withdrawal_penalty_percent(early_withdrawal_penalty_percent)
.staking_start_date(
if let Some(grid_data) = &node.grid_data {
// Try to get existing start date from grid_data
grid_data.get("staking_start_date")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc))
.unwrap_or_else(|| chrono::Utc::now())
} else {
// Set new start date if creating
chrono::Utc::now()
}
)
.build() {
Ok(staking_options) => {
// Store staking options in grid_data
if let Some(grid_data) = &mut node.grid_data {
if let Some(grid_obj) = grid_data.as_object_mut() {
grid_obj.insert("staking_options".to_string(), serde_json::to_value(staking_options).unwrap_or_default());
}
} else {
let mut grid_data = serde_json::Map::new();
grid_data.insert("staking_options".to_string(), serde_json::to_value(staking_options).unwrap_or_default());
node.grid_data = Some(serde_json::Value::Object(grid_data));
}
}
Err(e) => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
}
} else {
// Disable staking - return staked amount to wallet
// Check for existing staking in grid_data and return staked amount to wallet
if let Some(grid_data) = &node.grid_data {
if let Some(staking_data) = grid_data.get("staking_options") {
let staking_enabled = staking_data.get("staking_enabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if staking_enabled {
let return_amount = staking_data.get("staked_amount")
.and_then(|v| v.as_str())
.and_then(|s| rust_decimal::Decimal::from_str(s).ok())
.unwrap_or(rust_decimal::Decimal::ZERO);
let early_withdrawal_allowed = staking_data.get("early_withdrawal_allowed")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let penalty_percent = staking_data.get("early_withdrawal_penalty_percent")
.and_then(|v| v.as_f64())
.unwrap_or(25.0);
let penalty = if early_withdrawal_allowed {
return_amount * rust_decimal::Decimal::try_from(penalty_percent).unwrap_or_default() / rust_decimal::Decimal::from(100)
} else {
rust_decimal::Decimal::ZERO
};
persistent_data.wallet_balance_usd += return_amount - penalty;
}
}
}
// Remove staking options from grid_data
if let Some(grid_data) = &mut node.grid_data {
if let Some(grid_obj) = grid_data.as_object_mut() {
grid_obj.remove("staking_options");
}
}
}
}
// Save updated data
match UserPersistence::save_user_data(&persistent_data) {
Ok(_) => {
// TODO: Update marketplace products based on new configuration
// This would involve updating slice products and full node products
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to save node configuration",
"details": e.to_string()
})).build())
}
}
} else {
Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
/// Stake MC Credits on a node
pub async fn stake_on_node(
session: Session,
path: web::Path<String>,
form: web::Json<serde_json::Value>,
) -> Result<impl Responder> {
let node_id = path.into_inner();
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
Ok(None) => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Parse staking configuration
let staking_data = form.into_inner();
let staked_amount = staking_data.get("staked_amount")
.and_then(|v| v.as_str())
.and_then(|s| rust_decimal::Decimal::from_str(s).ok())
.unwrap_or(rust_decimal::Decimal::ZERO);
let staking_period_months = staking_data.get("staking_period_months")
.and_then(|v| v.as_u64())
.unwrap_or(3) as u32;
let early_withdrawal_allowed = staking_data.get("early_withdrawal_allowed")
.and_then(|v| v.as_bool())
.unwrap_or(true);
// Build staking options
let staking_options = match NodeStakingOptionsBuilder::new()
.staking_enabled(true)
.staked_amount(staked_amount)
.staking_period_months(staking_period_months)
.early_withdrawal_allowed(early_withdrawal_allowed)
.build() {
Ok(options) => options,
Err(e) => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Stake on node
match resource_provider_service.stake_on_node(&user_email, &node_id, staking_options) {
Ok(()) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Successfully staked ${} USD Credits on node", staked_amount)
})).build())
}
Err(e) => {
Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
}
/// Update staking on a node
pub async fn update_node_staking(
session: Session,
path: web::Path<String>,
form: web::Json<serde_json::Value>,
) -> Result<impl Responder> {
let node_id = path.into_inner();
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
Ok(None) => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Parse staking configuration
let staking_data = form.into_inner();
let action = staking_data.get("action")
.and_then(|v| v.as_str())
.unwrap_or("update");
match action {
"unstake" => {
// Unstake from node
match resource_provider_service.unstake_from_node(&user_email, &node_id) {
Ok(returned_amount) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Successfully unstaked ${} USD Credits from node", returned_amount),
"returned_amount": returned_amount
})).build())
}
Err(e) => {
Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
}
"update" => {
// Update staking amount
let staked_amount = staking_data.get("staked_amount")
.and_then(|v| v.as_str())
.and_then(|s| rust_decimal::Decimal::from_str(s).ok())
.unwrap_or(rust_decimal::Decimal::ZERO);
let staking_period_months = staking_data.get("staking_period_months")
.and_then(|v| v.as_u64())
.unwrap_or(3) as u32;
let early_withdrawal_allowed = staking_data.get("early_withdrawal_allowed")
.and_then(|v| v.as_bool())
.unwrap_or(true);
// Build new staking options
let staking_options = match NodeStakingOptionsBuilder::new()
.staking_enabled(true)
.staked_amount(staked_amount)
.staking_period_months(staking_period_months)
.early_withdrawal_allowed(early_withdrawal_allowed)
.build() {
Ok(options) => options,
Err(e) => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Update node staking
match resource_provider_service.update_node_staking(&user_email, &node_id, staking_options) {
Ok(()) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Successfully updated staking to ${} USD Credits", staked_amount)
})).build())
}
Err(e) => {
Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
}
_ => {
Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
}
/// Get staking statistics for a user
pub async fn get_staking_statistics(session: Session) -> Result<impl Responder> {
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
Ok(None) => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
let statistics = resource_provider_service.get_staking_statistics(&user_email);
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
/// Add user activity tracking
pub async fn add_user_activity(
session: Session,
activity_data: web::Json<serde_json::Value>
) -> Result<impl Responder> {
let user_service = match crate::services::user_service::UserService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
};
// Parse activity data
let activity_type_str = activity_data.get("activity_type")
.and_then(|v| v.as_str())
.unwrap_or("Unknown");
let activity_type = match activity_type_str {
"Login" => crate::models::user::ActivityType::Login,
"Purchase" => crate::models::user::ActivityType::Purchase,
"Deployment" => crate::models::user::ActivityType::Deployment,
"MarketplaceView" => crate::models::user::ActivityType::MarketplaceView,
_ => crate::models::user::ActivityType::MarketplaceView,
};
let description = activity_data.get("description")
.and_then(|v| v.as_str())
.unwrap_or("User activity")
.to_string();
// Create activity using builder pattern
let activity = match crate::models::builders::UserActivityBuilder::new()
.activity_type(activity_type)
.description(description)
.category("user".to_string())
.importance(crate::models::user::ActivityImportance::Medium)
.build()
{
Ok(activity) => activity,
Err(e) => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Save activity
match user_service.add_user_activity(&user_email, activity) {
Ok(_) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
}
/// Get user preferences
pub async fn get_user_preferences(session: Session) -> Result<impl Responder> {
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
};
if let Some(persistent_data) = UserPersistence::load_user_data(&user_email) {
Ok(ResponseBuilder::ok().json(persistent_data.user_preferences).build())
} else {
Ok(ResponseBuilder::ok().json(serde_json::Value::Null).build())
}
}
/// Update user preferences
pub async fn update_user_preferences(
session: Session,
preferences_data: web::Json<crate::models::user::UserPreferences>
) -> Result<impl Responder> {
let user_service = match crate::services::user_service::UserService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
};
match user_service.update_user_preferences(&user_email, preferences_data.into_inner()) {
Ok(_) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
}
/// Get user's service bookings for user dashboard
pub async fn get_user_service_bookings_api(session: Session) -> Result<impl Responder> {
// Get user email from session
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(ResponseBuilder::unauthorized()
.json(serde_json::json!({
"error": "User not authenticated"
}))
.build());
}
};
// Load user's service bookings from persistent storage
let service_bookings = UserPersistence::get_user_service_bookings(&user_email);
Ok(ResponseBuilder::ok()
.json(serde_json::json!({
"bookings": service_bookings,
"total": service_bookings.len()
}))
.build())
}
/// Refresh slice calculations for resource_provider
pub async fn refresh_slice_calculations(session: Session) -> Result<impl Responder> {
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
};
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
match resource_provider_service.refresh_all_slice_calculations(&user_email) {
Ok(_) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
}
/// Sync resource_provider nodes with ThreeFold Grid
pub async fn sync_with_grid(session: Session) -> Result<impl Responder> {
// Check authentication
if let Err(response) = Self::check_authentication(&session) {
return Ok(response);
}
// Mock sync operation
let sync_result = serde_json::json!({
"success": true,
"message": "Successfully synced with ThreeFold Grid",
"nodes_updated": 3,
"new_nodes_found": 1,
"sync_timestamp": chrono::Utc::now().to_rfc3339()
});
Ok(ResponseBuilder::ok().json(sync_result).build()?)
}
/// Renders the embedded shopping cart page within dashboard
pub async fn cart_section(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
// Check authentication
if let Err(response) = Self::check_authentication(&session) {
return Ok(response);
}
// Load user data
let user = match Self::load_user_with_persistent_data(&session) {
Some(user) => user,
None => {
return Ok(ResponseBuilder::redirect("/register").build()?);
}
};
// Prepare template context
let mut context = tera::Context::new();
// Add required template variables
let is_gitea_flow_active = get_app_config().is_gitea_enabled();
// Add user context for navbar authentication state
if let Ok(Some(user_json)) = session.get::<String>("user") {
context.insert("user_json", &user_json);
}
context.insert("user", &user);
context.insert("active_section", "cart");
context.insert("active_page", "cart");
context.insert("page_title", "Shopping Cart");
context.insert("gitea_enabled", &is_gitea_flow_active);
// Inject currency display variables
{
let currency_service = match crate::services::currency::CurrencyService::builder().build() {
Ok(svc) => svc,
Err(_) => crate::services::currency::CurrencyService::new(),
};
let display_currency = currency_service.get_user_preferred_currency(&session);
let currency_symbol = currency_service
.get_currency(&display_currency)
.map(|c| c.symbol)
.unwrap_or_else(|| "$".to_string());
context.insert("display_currency", &display_currency);
context.insert("currency_symbol", &currency_symbol);
}
// Render template
render_template(&tmpl, "dashboard/cart.html", &context)
}
/// Renders the embedded orders history page within dashboard
pub async fn orders_section(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
// Check authentication
if let Err(response) = Self::check_authentication(&session) {
return Ok(response);
}
// Load user data
let user = match Self::load_user_with_persistent_data(&session) {
Some(user) => user,
None => {
return Ok(ResponseBuilder::redirect("/register").build()?);
}
};
// Prepare template context
let mut context = tera::Context::new();
// Add required template variables
let is_gitea_flow_active = get_app_config().is_gitea_enabled();
// Add user context for navbar authentication state
if let Ok(Some(user_json)) = session.get::<String>("user") {
context.insert("user_json", &user_json);
}
context.insert("user", &user);
context.insert("active_section", "orders");
context.insert("active_page", "orders");
context.insert("page_title", "Order History");
context.insert("gitea_enabled", &is_gitea_flow_active);
// Inject currency display variables
{
let currency_service = match crate::services::currency::CurrencyService::builder().build() {
Ok(svc) => svc,
Err(_) => crate::services::currency::CurrencyService::new(),
};
let display_currency = currency_service.get_user_preferred_currency(&session);
let currency_symbol = currency_service
.get_currency(&display_currency)
.map(|c| c.symbol)
.unwrap_or_else(|| "$".to_string());
context.insert("display_currency", &display_currency);
context.insert("currency_symbol", &currency_symbol);
}
// Render template
render_template(&tmpl, "dashboard/orders.html", &context)
}
/// Get slices for a specific node
pub async fn get_node_slices(
session: Session,
path: web::Path<u32>
) -> Result<impl Responder> {
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
};
let node_id = path.into_inner();
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
match resource_provider_service.get_node_slices(&user_email, node_id) {
Ok(slices) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
}
/// API endpoint to validate grid nodes for automatic slice management
pub async fn validate_grid_nodes_automatic(session: Session, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
let node_ids: Vec<u32> = match form.get("node_ids").and_then(|v| v.as_array()) {
Some(ids) => {
ids.iter()
.filter_map(|id| id.as_u64().map(|n| n as u32))
.collect()
}
_ => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
if node_ids.is_empty() {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
// Initialize services
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
let slice_calculator = match crate::services::slice_calculator::SliceCalculatorService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Validate each node and calculate slices
let mut validated_nodes = Vec::new();
let mut errors = Vec::new();
for node_id in &node_ids {
match resource_provider_service.fetch_and_validate_grid_node(*node_id).await {
Ok(node_data) => {
// Calculate automatic slices
let total_base_slices = slice_calculator.calculate_max_base_slices(&node_data.capacity);
// Create a temporary FarmNode for slice calculation
let temp_node = crate::models::user::FarmNode {
id: format!("grid_node_{}", *node_id),
name: format!("Grid Node {}", *node_id),
location: node_data.location.clone(),
status: crate::models::user::NodeStatus::Online,
capacity: node_data.capacity.clone(),
used_capacity: crate::models::user::NodeCapacity {
cpu_cores: 0,
memory_gb: 0,
storage_gb: 0,
ram_gb: 0,
bandwidth_mbps: 0,
ssd_storage_gb: 0,
hdd_storage_gb: 0,
},
uptime_percentage: 99.0,
farming_start_date: chrono::Utc::now(),
last_updated: chrono::Utc::now(),
last_seen: Some(chrono::Utc::now()),
health_score: 98.5,
utilization_7_day_avg: 50.0,
slice_formats_supported: vec!["1x1".to_string(), "2x1".to_string()],
rental_options: None,
earnings_today_usd: rust_decimal::Decimal::ZERO,
region: if node_data.country.is_empty() { "Unknown".to_string() } else { node_data.country.clone() },
node_type: "MyceliumNode".to_string(),
slice_formats: None,
staking_options: None,
availability_status: crate::models::user::NodeAvailabilityStatus::Available,
grid_node_id: Some(node_id.to_string()),
grid_data: Some(serde_json::to_value(node_data.clone()).unwrap_or_default()),
node_group_id: None,
group_assignment_date: None,
group_slice_format: None,
group_slice_price: None,
// NEW: Marketplace SLA field (None for temp node)
marketplace_sla: None,
total_base_slices: total_base_slices as i32,
allocated_base_slices: 0,
slice_allocations: Vec::new(),
available_combinations: Vec::new(),
slice_pricing: Some(serde_json::to_value(crate::services::slice_calculator::SlicePricing::default()).unwrap_or_default()),
slice_last_calculated: Some(chrono::Utc::now()),
};
let available_combinations = slice_calculator.generate_slice_combinations(
total_base_slices,
0, // No allocated slices yet
&temp_node,
&user_email
);
validated_nodes.push(serde_json::json!({
"grid_node_id": *node_id,
"name": format!("Grid Node {}", *node_id),
"location": format!("{}, {}",
if node_data.city.is_empty() { "Unknown City" } else { &node_data.city },
if node_data.country.is_empty() { "Unknown Country" } else { &node_data.country }
),
"status": "Online",
"capacity": node_data.total_resources,
"total_base_slices": total_base_slices,
"available_combinations": available_combinations
}));
}
Err(e) => {
errors.push(serde_json::json!({
"node_id": node_id,
"error": e.to_string()
}));
// Only log as debug to avoid confusing users with error messages when operation succeeds
}
}
}
// Determine success based on whether any nodes were validated
let success = !validated_nodes.is_empty();
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": success,
"nodes": validated_nodes,
"errors": errors,
"total_requested": node_ids.len(),
"total_valid": validated_nodes.len(),
"partial_success": success && !errors.is_empty()
})).build())
}
/// API endpoint to add nodes with automatic slice management
pub async fn add_nodes_automatic(session: Session, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
// Parse form data
let node_ids: Vec<u32> = match form.get("node_ids").and_then(|v| v.as_array()) {
Some(ids) => {
ids.iter()
.filter_map(|id| id.as_u64().map(|n| n as u32))
.collect()
}
_ => {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
let base_slice_price = form.get("base_slice_price")
.and_then(|v| v.as_f64())
.map(|p| rust_decimal::Decimal::from_f64_retain(p).unwrap_or(rust_decimal::Decimal::new(50, 2)))
.unwrap_or(rust_decimal::Decimal::new(50, 2)); // Default $0.50 USD
let node_uptime_sla = form.get("node_uptime_sla")
.and_then(|v| v.as_f64())
.unwrap_or(99.8) as f32;
let node_bandwidth_sla = form.get("node_bandwidth_sla")
.and_then(|v| v.as_i64())
.unwrap_or(100) as i32;
let node_group = form.get("node_group")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let enable_staking = form.get("enable_staking")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let enable_full_node_rental = form.get("enable_full_node_rental")
.and_then(|v| v.as_bool())
.unwrap_or(false);
// Validate pricing range
if base_slice_price < rust_decimal::Decimal::new(10, 2) || base_slice_price > rust_decimal::Decimal::new(200, 2) {
return Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
// Initialize resource_provider service
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Add nodes with automatic slice management
match resource_provider_service.add_multiple_grid_nodes_with_automatic_slices(
&user_email,
node_ids.clone(),
base_slice_price,
node_uptime_sla,
node_bandwidth_sla,
node_group,
enable_staking,
enable_full_node_rental,
).await {
Ok(added_nodes) => {
// Add a small delay to ensure all backend operations are fully complete
actix_web::rt::time::sleep(std::time::Duration::from_millis(200)).await;
// Create response with detailed information
let response = serde_json::json!({
"success": true,
"message": format!("Successfully added {} nodes with automatic slice management", added_nodes.len()),
"nodes_added": added_nodes.len(),
"added_nodes": added_nodes,
"timestamp": chrono::Utc::now().to_rfc3339(),
"user_email": user_email
});
Ok(ResponseBuilder::ok().json(response).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Failed to add nodes with automatic slice management",
"details": e.to_string(),
"timestamp": chrono::Utc::now().to_rfc3339()
})).build())
}
}
}
/// API endpoint to refresh slice calculations for all resource_provider nodes
pub async fn refresh_slice_calculations_api(session: Session) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
// Initialize resource_provider service
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Refresh slice calculations for all nodes
match resource_provider_service.refresh_all_slice_calculations_async(&user_email).await {
Ok(updated_nodes) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Successfully refreshed slice calculations for {} nodes", updated_nodes),
"updated_nodes": updated_nodes
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to refresh slice calculations",
"details": e.to_string()
})).build())
}
}
}
/// API endpoint to sync with ThreeFold Grid
pub async fn sync_with_grid_api(session: Session) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
// Initialize resource_provider service
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Sync all nodes with grid
match resource_provider_service.sync_all_nodes_with_grid_async(&user_email).await {
Ok(synced_nodes) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Successfully synced {} nodes with ThreeFold Grid", synced_nodes),
"synced_nodes": synced_nodes
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to sync with ThreeFold Grid",
"details": e.to_string()
})).build())
}
}
}
/// API endpoint to get node slice details
pub async fn get_node_slices_api(session: Session, path: web::Path<String>) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
let node_id = path.into_inner();
// Initialize resource_provider service
let resource_provider_service = match crate::services::resource_provider::ResourceProviderService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Get node slice details
match resource_provider_service.get_node_slice_details(&user_email, &node_id) {
Ok(slice_details) => {
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"error": "Failed to get node slice details",
"details": e.to_string()
})).build())
}
}
}
/// API endpoint to get user's slice rentals
pub async fn get_user_slice_rentals(session: Session) -> Result<impl Responder> {
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
};
// Build slice rental service
let slice_rental_service = match crate::services::slice_rental::SliceRentalService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Get user's slice rentals
let slice_rentals = slice_rental_service.get_user_slice_rentals(&user_email);
// Transform rentals for frontend display
let rental_data: Vec<serde_json::Value> = slice_rentals.iter().map(|rental| {
serde_json::json!({
"id": rental.rental_id,
"slice_combination_id": rental.slice_combination_id,
"deployment_type": rental.deployment_type.as_ref().unwrap_or(&"vm".to_string()),
"deployment_name": rental.deployment_name.as_ref().unwrap_or(&format!("Deployment-{}", &rental.rental_id[..8])),
"deployment_status": rental.deployment_status.as_ref().unwrap_or(&"provisioning".to_string()),
"deployment_endpoint": rental.deployment_endpoint,
"specs": format!("{} vCPU, {}GB RAM, {}GB Storage",
rental.slice_allocation.base_slices_used,
rental.slice_allocation.base_slices_used * 4,
rental.slice_allocation.base_slices_used * 200
),
"monthly_cost": rental.slice_allocation.monthly_cost,
"status": match rental.slice_allocation.status {
crate::services::slice_calculator::AllocationStatus::Active => "active",
crate::services::slice_calculator::AllocationStatus::Expired => "expired",
crate::services::slice_calculator::AllocationStatus::Cancelled => "cancelled",
},
"created_at": rental.slice_allocation.rental_start.format("%Y-%m-%d %H:%M:%S").to_string(),
"expires_at": rental.slice_allocation.rental_end
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_else(|| "Never".to_string()),
"metadata": rental.deployment_metadata
})
}).collect();
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
/// API endpoint to manage slice rental deployments (start/stop/restart)
pub async fn manage_slice_rental_deployment(
session: Session,
path: web::Path<String>,
form: web::Json<serde_json::Value>
) -> Result<impl Responder> {
let rental_id = path.into_inner();
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
};
let action = form.get("action").and_then(|v| v.as_str()).unwrap_or("");
// Build slice rental service
let slice_rental_service = match crate::services::slice_rental::SliceRentalService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Get user's slice rentals to verify ownership
let slice_rentals = slice_rental_service.get_user_slice_rentals(&user_email);
let rental = slice_rentals.iter().find(|r| r.rental_id == rental_id);
if rental.is_none() {
return Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
match action {
"start" => {
// Add activity record
let activity = crate::models::builders::UserActivityBuilder::new()
.activity_type(crate::models::user::ActivityType::SliceRentalStarted)
.description(format!("Started deployment for slice rental: {}", rental_id))
.category("Slice Rental".to_string())
.importance(crate::models::user::ActivityImportance::Medium)
.build()
.unwrap();
let _ = crate::services::user_persistence::UserPersistence::add_user_activity(&user_email, activity);
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Deployment started for slice rental: {}", rental_id),
"status": "starting"
})).build())
}
"stop" => {
// Add activity record
let activity = crate::models::builders::UserActivityBuilder::new()
.activity_type(crate::models::user::ActivityType::SliceRentalStopped)
.description(format!("Stopped deployment for slice rental: {}", rental_id))
.category("Slice Rental".to_string())
.importance(crate::models::user::ActivityImportance::Medium)
.build()
.unwrap();
let _ = crate::services::user_persistence::UserPersistence::add_user_activity(&user_email, activity);
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Deployment stopped for slice rental: {}", rental_id),
"status": "stopped"
})).build())
}
"restart" => {
// Add activity record
let activity = crate::models::builders::UserActivityBuilder::new()
.activity_type(crate::models::user::ActivityType::SliceRentalRestarted)
.description(format!("Restarted deployment for slice rental: {}", rental_id))
.category("Slice Rental".to_string())
.importance(crate::models::user::ActivityImportance::Medium)
.build()
.unwrap();
let _ = crate::services::user_persistence::UserPersistence::add_user_activity(&user_email, activity);
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Deployment restarted for slice rental: {}", rental_id),
"status": "restarting"
})).build())
}
_ => {
Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
}
/// API endpoint to cancel/terminate a slice rental
pub async fn cancel_slice_rental(
session: Session,
path: web::Path<String>
) -> Result<impl Responder> {
let rental_id = path.into_inner();
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
};
// Build slice rental service
let slice_rental_service = match crate::services::slice_rental::SliceRentalService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Get user's slice rentals to verify ownership
let slice_rentals = slice_rental_service.get_user_slice_rentals(&user_email);
let rental = slice_rentals.iter().find(|r| r.rental_id == rental_id);
if rental.is_none() {
return Ok(ResponseBuilder::not_found().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
// In a real implementation, this would:
// 1. Stop all deployments on the slice
// 2. Release the slice allocation
// 3. Update the rental status to cancelled
// 4. Process any refunds if applicable
// 5. Remove from user's active rentals
// Add activity record
let activity = crate::models::builders::UserActivityBuilder::new()
.activity_type(crate::models::user::ActivityType::SliceRentalCancelled)
.description(format!("Cancelled slice rental: {}", rental_id))
.category("Slice Rental".to_string())
.importance(crate::models::user::ActivityImportance::High)
.build()
.unwrap();
let _ = crate::services::user_persistence::UserPersistence::add_user_activity(&user_email, activity);
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Slice rental {} has been cancelled successfully", rental_id)
})).build())
}
/// Create a new product/application from service provider dashboard
pub async fn create_product(session: Session, product_data: web::Json<serde_json::Value>) -> Result<impl Responder> {
// Check authentication
if let Err(response) = Self::check_authentication(&session) {
return Ok(response);
}
let user_email = session.get::<String>("user_email").unwrap_or_default().unwrap_or_default();
// Parse product data from JSON
let name = product_data.get("name")
.and_then(|v| v.as_str())
.unwrap_or("Unnamed Application")
.to_string();
let description = product_data.get("description")
.and_then(|v| v.as_str())
.unwrap_or("No description provided")
.to_string();
let category_id = product_data.get("category")
.and_then(|v| v.as_str())
.unwrap_or("service")
.to_string();
let base_price = product_data.get("price")
.and_then(|v| v.as_f64())
.map(|p| rust_decimal::Decimal::from_f64_retain(p).unwrap_or(rust_decimal::Decimal::new(0, 0)))
.unwrap_or(rust_decimal::Decimal::new(0, 0));
let base_currency = product_data.get("currency")
.and_then(|v| v.as_str())
.unwrap_or("USD")
.to_string();
// Get provider name from user data
let user = Self::load_user_with_persistent_data(&session);
let provider_name = user.as_ref().map(|u| u.name.clone()).unwrap_or_else(|| user_email.clone());
// Create product ID
let product_id = format!("user_{}_{}",
user_email.replace("@", "_").replace(".", "_"),
uuid::Uuid::new_v4().to_string()[0..8].to_string()
);
// Create product attributes map
let mut attributes = std::collections::HashMap::new();
// Add any additional attributes from the request
if let Some(attrs) = product_data.get("attributes").and_then(|v| v.as_object()) {
for (key, value) in attrs {
attributes.insert(key.clone(), crate::models::product::ProductAttribute {
key: key.clone(),
value: value.clone(),
attribute_type: crate::models::product::AttributeType::Text,
is_searchable: true,
is_filterable: false,
display_order: None,
});
}
}
// Create availability from status
let availability = match product_data.get("availability")
.and_then(|v| v.as_str())
.unwrap_or("Available") {
"Available" => crate::models::product::ProductAvailability::Available,
"Limited" => crate::models::product::ProductAvailability::Limited,
"Unavailable" => crate::models::product::ProductAvailability::Unavailable,
"PreOrder" => crate::models::product::ProductAvailability::PreOrder,
other => crate::models::product::ProductAvailability::Custom(other.to_string()),
};
// Create product metadata
let metadata = crate::models::product::ProductMetadata {
tags: product_data.get("tags")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default(),
location: product_data.get("location").and_then(|v| v.as_str()).map(String::from),
rating: None,
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(),
};
// Create the product
let product = crate::models::product::Product {
id: product_id,
name,
category_id,
description,
base_price,
base_currency,
attributes,
provider_id: user_email.clone(),
provider_name,
availability,
metadata,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
// Save the product to user's persistent data
match crate::services::user_persistence::UserPersistence::add_user_product(&user_email, product.clone()) {
Ok(_) => {
ResponseBuilder::ok().status(201).json(serde_json::json!({
"success": true,
"message": "Product created successfully",
"product": product
})).build()
}
Err(e) => {
ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Failed to create product",
"details": format!("{}", e)
})).build()
}
}
}
/// Get all products for the authenticated user
pub async fn get_user_products(session: Session) -> Result<impl Responder> {
// Check authentication
if let Err(response) = Self::check_authentication(&session) {
return Ok(response);
}
let user_email = session.get::<String>("user_email").unwrap_or_default().unwrap_or_default();
let products = crate::services::user_persistence::UserPersistence::get_user_products(&user_email);
ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"products": products
})).build()
}
}
/// Form data structures for settings updates
#[derive(Debug, Deserialize)]
pub struct ProfileUpdateForm {
pub name: String,
pub country: String,
pub timezone: String,
}
#[derive(Debug, Deserialize)]
pub struct PasswordUpdateForm {
pub current_password: String,
pub new_password: String,
pub confirm_password: String,
}
#[derive(Debug, Deserialize)]
pub struct NotificationUpdateForm {
pub email_security_alerts: bool,
pub email_billing_alerts: bool,
pub email_system_alerts: bool,
pub email_newsletter: bool,
pub dashboard_alerts: bool,
pub dashboard_updates: bool,
}
#[derive(Debug, Deserialize)]
pub struct AddTfpForm {
pub amount: i32,
}
#[derive(Debug, Deserialize)]
pub struct VerifyPasswordForm {
pub password: String,
}
#[derive(Debug, Deserialize)]
pub struct DeleteAccountForm {
pub confirmation: String,
pub password: String,
}
/// API endpoint for user dashboard data
pub async fn user_dashboard_data_api(session: Session) -> Result<impl Responder> {
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Build user service
let user_service = match crate::services::user_service::UserService::builder().build() {
Ok(service) => service,
Err(e) => {
return Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
// Load comprehensive user dashboard data
let user_applications = user_service.get_user_applications(&user_email);
let user_services = user_service.get_user_purchased_services(&user_email);
let compute_resources = user_service.get_user_compute_resources(&user_email);
let recent_activities = user_service.get_user_activities(&user_email, Some(20));
let user_metrics = user_service.calculate_user_metrics(&user_email);
let usage_statistics = user_service.get_usage_statistics(&user_email);
// Load slice rental data
let slice_rental_data = if let Ok(slice_rental_service) = crate::services::slice_rental::SliceRentalService::builder().build() {
let user_slice_rentals = slice_rental_service.get_user_slice_rentals(&user_email);
// Calculate deployment type breakdown
let mut vm_count = 0;
let k8s_count = 0;
let mut total_monthly_cost = rust_decimal::Decimal::ZERO;
for rental in &user_slice_rentals {
total_monthly_cost += rental.slice_allocation.monthly_cost;
// For now, we'll assume all rentals are VM deployments
// In a real implementation, this would be stored in the rental metadata
vm_count += 1;
}
serde_json::json!({
"active_rentals": user_slice_rentals,
"stats": {
"total_active_rentals": user_slice_rentals.len(),
"total_monthly_cost": total_monthly_cost,
"vm_deployments": vm_count,
"kubernetes_deployments": k8s_count
}
})
} else {
serde_json::json!({
"active_rentals": [],
"stats": {
"total_active_rentals": 0,
"total_monthly_cost": 0,
"vm_deployments": 0,
"kubernetes_deployments": 0
}
})
};
// Prepare comprehensive response
let response_data = serde_json::json!({
"applications": user_applications,
"services": user_services,
"compute_resources": compute_resources,
"recent_activities": recent_activities,
"user_metrics": user_metrics,
"usage_statistics": usage_statistics,
"slice_rentals": slice_rental_data,
"usd_usage_trend": user_metrics.cost_trend,
"resource_utilization": user_metrics.resource_utilization,
"user_activity": {
"deployments": [2, 3, 1, 4, 2, 3], // Mock data for chart
"resource_reservations": [1, 2, 2, 3, 1, 2] // Mock data for chart
},
"deployment_distribution": {
"regions": [
{"region": "Amsterdam", "apps": 2, "nodes": 1, "slices": 4},
{"region": "New York", "apps": 1, "nodes": 0, "slices": 2},
{"region": "Singapore", "apps": 0, "nodes": 1, "slices": 3}
]
}
});
Ok(ResponseBuilder::ok().json(response_data).build())
}
/// API endpoint for managing user slice rentals
pub async fn manage_slice_rental(
path: web::Path<String>,
form: web::Json<serde_json::Value>,
session: Session,
) -> Result<impl Responder> {
let rental_id = path.into_inner();
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
};
let action = form.get("action").and_then(|v| v.as_str()).unwrap_or("");
match action {
"terminate" => {
// Implement slice rental termination
// 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::SliceReleased,
description: format!("Terminated slice rental {}", rental_id),
timestamp: Utc::now(),
metadata: None,
category: "Slice Management".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);
}
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
"resize" => {
let new_quantity = form.get("quantity").and_then(|v| v.as_u64()).unwrap_or(1) as u32;
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"message": format!("Slice rental resized to {} slices", new_quantity)
})).build())
}
_ => {
Ok(ResponseBuilder::bad_request().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
}
/// API endpoint to cleanup duplicate nodes and fix data inconsistencies
pub async fn cleanup_data(session: Session) -> Result<impl Responder> {
let user_email = session.get::<String>("user_email")
.unwrap_or_default()
.unwrap_or_default();
if user_email.is_empty() {
return Ok(ResponseBuilder::unauthorized().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build());
}
// Run data cleanup
match DataCleanup::remove_duplicate_nodes(&user_email) {
Ok(duplicates_removed) => {
// Also validate and fix slice data
let slice_fixes = DataCleanup::validate_slice_data(&user_email)
.unwrap_or(false);
Ok(ResponseBuilder::ok().json(serde_json::json!({
"success": true,
"duplicates_removed": duplicates_removed,
"slice_data_fixed": slice_fixes,
"message": format!("Cleanup complete: {} duplicates removed", duplicates_removed)
})).build())
}
Err(e) => {
Ok(ResponseBuilder::internal_error().json(serde_json::json!({
"success": false,
"error": "Pattern needs manual fix"
})).build())
}
}
}
// =============================================================================
// SSH KEY MANAGEMENT ENDPOINTS
// =============================================================================
impl DashboardController {
/// Get all SSH keys for the authenticated user
pub async fn get_ssh_keys(session: Session) -> Result<impl Responder> {
// Check authentication
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => {
return Ok(ResponseBuilder::unauthorized()
.message("Authentication required")
.build()?);
}
};
// Build SSH key service
let ssh_service = match crate::services::ssh_key_service::SSHKeyService::builder().build() {
Ok(service) => service,
Err(e) => {
log::error!("Failed to build SSH key service: {}", e);
return Ok(ResponseBuilder::internal_error()
.message("Service unavailable")
.build()?);
}
};
// Get user's SSH keys
let req_id = uuid::Uuid::new_v4().to_string();
let ssh_keys = ssh_service.get_user_ssh_keys_async(&user_email, Some(&req_id)).await;
Ok(ResponseBuilder::success()
.data(serde_json::json!({
"ssh_keys": ssh_keys,
"count": ssh_keys.len()
}))
.message("SSH keys retrieved successfully")
.build()?)
}
/// Add a new SSH key for the authenticated user
pub async fn add_ssh_key(session: Session, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
// Check authentication
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => {
return Ok(ResponseBuilder::unauthorized()
.message("Authentication required")
.build()?);
}
};
// Extract form data
let name = match form.get("name").and_then(|v| v.as_str()) {
Some(name) if !name.trim().is_empty() => name.trim(),
_ => {
return Ok(ResponseBuilder::bad_request()
.message("SSH key name is required")
.build()?);
}
};
let public_key = match form.get("public_key").and_then(|v| v.as_str()) {
Some(key) if !key.trim().is_empty() => key.trim(),
_ => {
return Ok(ResponseBuilder::bad_request()
.message("SSH public key is required")
.build()?);
}
};
let is_default = form.get("is_default")
.and_then(|v| v.as_bool())
.unwrap_or(false);
// Build SSH key service
let ssh_service = match crate::services::ssh_key_service::SSHKeyService::builder().build() {
Ok(service) => service,
Err(e) => {
log::error!("Failed to build SSH key service: {}", e);
return Ok(ResponseBuilder::internal_error()
.message("Service unavailable")
.build()?);
}
};
// Add SSH key
let req_id = uuid::Uuid::new_v4().to_string();
match ssh_service.add_ssh_key_async(&user_email, name, public_key, is_default, Some(&req_id)).await {
Ok(ssh_key) => {
log::info!("SSH key added successfully for user {}: {}", user_email, ssh_key.id);
Ok(ResponseBuilder::success()
.data(serde_json::json!({
"ssh_key": ssh_key
}))
.message("SSH key added successfully")
.build()?)
}
Err(e) => {
log::warn!("Failed to add SSH key for user {}: {}", user_email, e);
Ok(ResponseBuilder::bad_request()
.message(&e.to_string())
.build()?)
}
}
}
/// Update an SSH key (name or default status)
pub async fn update_ssh_key(session: Session, path: web::Path<String>, form: web::Json<serde_json::Value>) -> Result<impl Responder> {
let key_id = path.into_inner();
// Check authentication
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => {
return Ok(ResponseBuilder::unauthorized()
.message("Authentication required")
.build()?);
}
};
// Extract form data
let name = form.get("name").and_then(|v| v.as_str());
let is_default = form.get("is_default").and_then(|v| v.as_bool());
if name.is_none() && is_default.is_none() {
return Ok(ResponseBuilder::bad_request()
.message("At least one field (name or is_default) must be provided")
.build()?);
}
// Build SSH key service
let ssh_service = match crate::services::ssh_key_service::SSHKeyService::builder().build() {
Ok(service) => service,
Err(e) => {
log::error!("Failed to build SSH key service: {}", e);
return Ok(ResponseBuilder::internal_error()
.message("Service unavailable")
.build()?);
}
};
// Update SSH key
let req_id = uuid::Uuid::new_v4().to_string();
match ssh_service.update_ssh_key_async(&user_email, &key_id, name, is_default, Some(&req_id)).await {
Ok(ssh_key) => {
log::info!("SSH key updated successfully for user {}: {}", user_email, key_id);
Ok(ResponseBuilder::success()
.data(serde_json::json!({
"ssh_key": ssh_key
}))
.message("SSH key updated successfully")
.build()?)
}
Err(e) => {
log::warn!("Failed to update SSH key {} for user {}: {}", key_id, user_email, e);
Ok(ResponseBuilder::bad_request()
.message(&e.to_string())
.build()?)
}
}
}
/// Delete an SSH key
pub async fn delete_ssh_key(session: Session, path: web::Path<String>) -> Result<impl Responder> {
let key_id = path.into_inner();
// Check authentication
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => {
return Ok(ResponseBuilder::unauthorized()
.message("Authentication required")
.build()?);
}
};
// Build SSH key service
let ssh_service = match crate::services::ssh_key_service::SSHKeyService::builder().build() {
Ok(service) => service,
Err(e) => {
log::error!("Failed to build SSH key service: {}", e);
return Ok(ResponseBuilder::internal_error()
.message("Service unavailable")
.build()?);
}
};
// Delete SSH key
let req_id = uuid::Uuid::new_v4().to_string();
match ssh_service.delete_ssh_key_async(&user_email, &key_id, Some(&req_id)).await {
Ok(()) => {
log::info!("SSH key deleted successfully for user {}: {}", user_email, key_id);
Ok(ResponseBuilder::success()
.message("SSH key deleted successfully")
.build()?)
}
Err(e) => {
log::warn!("Failed to delete SSH key {} for user {}: {}", key_id, user_email, e);
Ok(ResponseBuilder::bad_request()
.message(&e.to_string())
.build()?)
}
}
}
/// Set an SSH key as default
pub async fn set_default_ssh_key(session: Session, path: web::Path<String>) -> Result<impl Responder> {
let key_id = path.into_inner();
// Check authentication
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => {
return Ok(ResponseBuilder::unauthorized()
.message("Authentication required")
.build()?);
}
};
// Build SSH key service
let ssh_service = match crate::services::ssh_key_service::SSHKeyService::builder().build() {
Ok(service) => service,
Err(e) => {
log::error!("Failed to build SSH key service: {}", e);
return Ok(ResponseBuilder::internal_error()
.message("Service unavailable")
.build()?);
}
};
// Set SSH key as default
let req_id = uuid::Uuid::new_v4().to_string();
match ssh_service.set_default_ssh_key_async(&user_email, &key_id, Some(&req_id)).await {
Ok(()) => {
log::info!("SSH key set as default for user {}: {}", user_email, key_id);
Ok(ResponseBuilder::success()
.message("SSH key set as default successfully")
.build()?)
}
Err(e) => {
log::warn!("Failed to set default SSH key {} for user {}: {}", key_id, user_email, e);
Ok(ResponseBuilder::bad_request()
.message(&e.to_string())
.build()?)
}
}
}
/// Get SSH key details by ID
pub async fn get_ssh_key_details(session: Session, path: web::Path<String>) -> Result<impl Responder> {
let key_id = path.into_inner();
// Check authentication
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => {
return Ok(ResponseBuilder::unauthorized()
.message("Authentication required")
.build()?);
}
};
// Build SSH key service
let ssh_service = match crate::services::ssh_key_service::SSHKeyService::builder().build() {
Ok(service) => service,
Err(e) => {
log::error!("Failed to build SSH key service: {}", e);
return Ok(ResponseBuilder::internal_error()
.message("Service unavailable")
.build()?);
}
};
// Get SSH key details
let req_id = uuid::Uuid::new_v4().to_string();
match ssh_service.get_ssh_key_by_id_async(&user_email, &key_id, Some(&req_id)).await {
Some(ssh_key) => {
Ok(ResponseBuilder::success()
.data(serde_json::json!({
"ssh_key": ssh_key
}))
.message("SSH key retrieved successfully")
.build()?)
}
None => {
Ok(ResponseBuilder::not_found()
.message("SSH key not found")
.build()?)
}
}
}
}