7800 lines
340 KiB
Rust
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", ¤cy_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", ¤cy_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", ¤cy_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", ¤cy_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()?)
|
|
}
|
|
}
|
|
}
|
|
}
|