use axum::{ extract::{Path, Query, State}, http::StatusCode, response::{Html, Json}, routing::{get, post}, Router, }; use chrono::{DateTime, Utc}; use diesel::prelude::*; use diesel::sql_types::{Nullable, Text, Timestamptz, Uuid as DieselUuid, Varchar}; use lettre::{Message, SmtpTransport, Transport}; use lettre::transport::smtp::authentication::Credentials; use log::{info, warn}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; // ============================================================================ // Invitation Email Functions // ============================================================================ /// Send invitation email via SMTP async fn send_invitation_email( to_email: &str, role: &str, custom_message: Option<&str>, invitation_id: Uuid, ) -> Result<(), String> { let smtp_host = std::env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string()); let smtp_user = std::env::var("SMTP_USER").ok(); let smtp_pass = std::env::var("SMTP_PASS").ok(); let smtp_from = std::env::var("SMTP_FROM").unwrap_or_else(|_| "noreply@generalbots.com".to_string()); let app_url = std::env::var("APP_URL").unwrap_or_else(|_| "https://app.generalbots.com".to_string()); let accept_url = format!("{}/accept-invitation?token={}", app_url, invitation_id); let body = format!( r#"You have been invited to join our organization as a {role}. {custom_msg} Click the link below to accept the invitation: {accept_url} This invitation will expire in 7 days. If you did not expect this invitation, you can safely ignore this email. Best regards, The General Bots Team"#, role = role, custom_msg = custom_message.unwrap_or(""), accept_url = accept_url ); let email = Message::builder() .from(smtp_from.parse().map_err(|e| format!("Invalid from address: {}", e))?) .to(to_email.parse().map_err(|e| format!("Invalid to address: {}", e))?) .subject("You've been invited to join our organization") .body(body) .map_err(|e| format!("Failed to build email: {}", e))?; let mailer = if let (Some(user), Some(pass)) = (smtp_user, smtp_pass) { let creds = Credentials::new(user, pass); SmtpTransport::relay(&smtp_host) .map_err(|e| format!("SMTP relay error: {}", e))? .credentials(creds) .build() } else { SmtpTransport::builder_dangerous(&smtp_host).build() }; mailer.send(&email).map_err(|e| format!("Failed to send email: {}", e))?; info!("Invitation email sent successfully to {}", to_email); Ok(()) } /// Send invitation email by fetching details from database async fn send_invitation_email_by_id(invitation_id: Uuid) -> Result<(), String> { let smtp_host = std::env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string()); let smtp_user = std::env::var("SMTP_USER").ok(); let smtp_pass = std::env::var("SMTP_PASS").ok(); let smtp_from = std::env::var("SMTP_FROM").unwrap_or_else(|_| "noreply@generalbots.com".to_string()); let app_url = std::env::var("APP_URL").unwrap_or_else(|_| "https://app.generalbots.com".to_string()); // Get database URL and connect let database_url = std::env::var("DATABASE_URL") .map_err(|_| "DATABASE_URL not configured".to_string())?; let mut conn = diesel::PgConnection::establish(&database_url) .map_err(|e| format!("Database connection failed: {}", e))?; // Fetch invitation details #[derive(QueryableByName)] struct InvitationDetails { #[diesel(sql_type = Varchar)] email: String, #[diesel(sql_type = Varchar)] role: String, #[diesel(sql_type = Nullable)] message: Option, } let invitation: InvitationDetails = diesel::sql_query( "SELECT email, role, message FROM organization_invitations WHERE id = $1 AND status = 'pending'" ) .bind::(invitation_id) .get_result(&mut conn) .map_err(|e| format!("Failed to fetch invitation: {}", e))?; let accept_url = format!("{}/accept-invitation?token={}", app_url, invitation_id); let body = format!( r#"You have been invited to join our organization as a {role}. {custom_msg} Click the link below to accept the invitation: {accept_url} This invitation will expire in 7 days. If you did not expect this invitation, you can safely ignore this email. Best regards, The General Bots Team"#, role = invitation.role, custom_msg = invitation.message.as_deref().unwrap_or(""), accept_url = accept_url ); let email = Message::builder() .from(smtp_from.parse().map_err(|e| format!("Invalid from address: {}", e))?) .to(invitation.email.parse().map_err(|e| format!("Invalid to address: {}", e))?) .subject("Reminder: You've been invited to join our organization") .body(body) .map_err(|e| format!("Failed to build email: {}", e))?; let mailer = if let (Some(user), Some(pass)) = (smtp_user, smtp_pass) { let creds = Credentials::new(user, pass); SmtpTransport::relay(&smtp_host) .map_err(|e| format!("SMTP relay error: {}", e))? .credentials(creds) .build() } else { SmtpTransport::builder_dangerous(&smtp_host).build() }; mailer.send(&email).map_err(|e| format!("Failed to send email: {}", e))?; info!("Invitation resend email sent successfully to {}", invitation.email); Ok(()) } use crate::core::urls::ApiUrls; use crate::core::middleware::AuthenticatedUser; use crate::shared::state::AppState; #[derive(Debug, Deserialize)] pub struct ConfigUpdateRequest { pub config_key: String, pub config_value: serde_json::Value, } #[derive(Debug, Deserialize)] pub struct MaintenanceScheduleRequest { pub scheduled_at: DateTime, pub duration_minutes: u32, pub reason: String, pub notify_users: bool, } #[derive(Debug, Deserialize)] pub struct BackupRequest { pub backup_type: String, pub include_files: bool, pub include_database: bool, pub compression: Option, } #[derive(Debug, Deserialize)] pub struct RestoreRequest { pub backup_id: String, pub restore_point: DateTime, pub verify_before_restore: bool, } #[derive(Debug, Deserialize)] pub struct UserManagementRequest { pub user_id: Uuid, pub action: String, pub reason: Option, } #[derive(Debug, Deserialize)] pub struct RoleManagementRequest { pub role_name: String, pub permissions: Vec, pub description: Option, } #[derive(Debug, Deserialize)] pub struct QuotaManagementRequest { pub user_id: Option, pub group_id: Option, pub quota_type: String, pub limit_value: u64, } #[derive(Debug, Deserialize)] pub struct LicenseManagementRequest { pub license_key: String, pub license_type: String, } #[derive(Debug, Deserialize)] pub struct LogQuery { pub start_date: Option, pub end_date: Option, pub level: Option, pub service: Option, pub limit: Option, } #[derive(Debug, Serialize)] pub struct SystemStatusResponse { pub status: String, pub uptime_seconds: u64, pub version: String, pub services: Vec, pub health_checks: Vec, pub last_restart: DateTime, } #[derive(Debug, Serialize)] pub struct ServiceStatus { pub name: String, pub status: String, pub uptime_seconds: u64, pub memory_mb: f64, pub cpu_percent: f64, } #[derive(Debug, Serialize)] pub struct HealthCheck { pub name: String, pub status: String, pub message: Option, pub last_check: DateTime, } #[derive(Debug, Serialize)] pub struct SystemMetricsResponse { pub cpu_usage: f64, pub memory_total_mb: u64, pub memory_used_mb: u64, pub memory_percent: f64, pub disk_total_gb: u64, pub disk_used_gb: u64, pub disk_percent: f64, pub network_in_mbps: f64, pub network_out_mbps: f64, pub active_connections: u32, pub request_rate_per_minute: u32, pub error_rate_percent: f64, } #[derive(Debug, Serialize)] pub struct LogEntry { pub id: Uuid, pub timestamp: DateTime, pub level: String, pub service: String, pub message: String, pub metadata: Option, } // ============================================================================= // INVITATION MANAGEMENT TYPES // ============================================================================= #[derive(Debug, Deserialize)] pub struct CreateInvitationRequest { pub email: String, #[serde(default = "default_role")] pub role: String, pub message: Option, } fn default_role() -> String { "member".to_string() } #[derive(Debug, Deserialize)] pub struct BulkInvitationRequest { pub emails: Vec, #[serde(default = "default_role")] pub role: String, pub message: Option, } #[derive(Debug, Serialize, QueryableByName)] pub struct InvitationRow { #[diesel(sql_type = DieselUuid)] pub id: Uuid, #[diesel(sql_type = DieselUuid)] pub org_id: Uuid, #[diesel(sql_type = Varchar)] pub email: String, #[diesel(sql_type = Varchar)] pub role: String, #[diesel(sql_type = Varchar)] pub status: String, #[diesel(sql_type = Nullable)] pub message: Option, #[diesel(sql_type = DieselUuid)] pub invited_by: Uuid, #[diesel(sql_type = Timestamptz)] pub created_at: DateTime, #[diesel(sql_type = Nullable)] pub expires_at: Option>, #[diesel(sql_type = Nullable)] pub accepted_at: Option>, } #[derive(Debug, Serialize)] pub struct InvitationResponse { pub success: bool, pub id: Option, pub email: Option, pub error: Option, } #[derive(Debug, Serialize)] pub struct BulkInvitationResponse { pub success: bool, pub sent: i32, pub failed: i32, pub errors: Vec, } #[derive(Debug, Serialize)] pub struct ConfigResponse { pub configs: Vec, pub last_updated: DateTime, } #[derive(Debug, Serialize)] pub struct ConfigItem { pub key: String, pub value: serde_json::Value, pub description: Option, pub editable: bool, pub requires_restart: bool, } #[derive(Debug, Serialize)] pub struct MaintenanceResponse { pub id: Uuid, pub scheduled_at: DateTime, pub duration_minutes: u32, pub reason: String, pub status: String, pub created_by: String, } #[derive(Debug, Serialize)] pub struct BackupResponse { pub id: Uuid, pub backup_type: String, pub size_bytes: u64, pub created_at: DateTime, pub status: String, pub download_url: Option, pub expires_at: Option>, } #[derive(Debug, Serialize)] pub struct QuotaResponse { pub id: Uuid, pub entity_type: String, pub entity_id: Uuid, pub quota_type: String, pub limit_value: u64, pub current_value: u64, pub percent_used: f64, } #[derive(Debug, Serialize)] pub struct LicenseResponse { pub id: Uuid, pub license_type: String, pub status: String, pub max_users: u32, pub current_users: u32, pub features: Vec, pub issued_at: DateTime, pub expires_at: Option>, } #[derive(Debug, Serialize)] pub struct SuccessResponse { pub success: bool, pub message: Option, } #[derive(Debug, Serialize)] pub struct AdminDashboardData { pub total_users: i64, pub active_groups: i64, pub running_bots: i64, pub storage_used_gb: f64, pub storage_total_gb: f64, pub recent_activity: Vec, pub system_health: SystemHealth, } #[derive(Debug, Serialize)] pub struct ActivityItem { pub id: String, pub action: String, pub user: String, pub timestamp: DateTime, pub details: Option, } #[derive(Debug, Serialize)] pub struct SystemHealth { pub status: String, pub cpu_percent: f64, pub memory_percent: f64, pub services_healthy: i32, pub services_total: i32, } #[derive(Debug, Serialize)] pub struct StatValue { pub value: String, pub label: String, pub trend: Option, } pub fn configure() -> Router> { Router::new() .route(ApiUrls::ADMIN_DASHBOARD, get(get_admin_dashboard)) .route(ApiUrls::ADMIN_STATS_USERS, get(get_stats_users)) .route(ApiUrls::ADMIN_STATS_GROUPS, get(get_stats_groups)) .route(ApiUrls::ADMIN_STATS_BOTS, get(get_stats_bots)) .route(ApiUrls::ADMIN_STATS_STORAGE, get(get_stats_storage)) .route(ApiUrls::ADMIN_USERS, get(get_admin_users)) .route(ApiUrls::ADMIN_GROUPS, get(get_admin_groups).post(create_group)) .route(ApiUrls::ADMIN_BOTS, get(get_admin_bots)) .route(ApiUrls::ADMIN_DNS, get(get_admin_dns)) .route(ApiUrls::ADMIN_BILLING, get(get_admin_billing)) .route(ApiUrls::ADMIN_AUDIT, get(get_admin_audit)) .route(ApiUrls::ADMIN_SYSTEM, get(get_system_status)) .route("/api/admin/export-report", get(export_admin_report)) .route("/api/admin/dashboard/stats", get(get_dashboard_stats)) .route("/api/admin/dashboard/health", get(get_dashboard_health)) .route("/api/admin/dashboard/activity", get(get_dashboard_activity)) .route("/api/admin/dashboard/members", get(get_dashboard_members)) .route("/api/admin/dashboard/roles", get(get_dashboard_roles)) .route("/api/admin/dashboard/bots", get(get_dashboard_bots)) .route("/api/admin/dashboard/invitations", get(get_dashboard_invitations)) .route("/api/admin/invitations", get(list_invitations).post(create_invitation)) .route("/api/admin/invitations/bulk", post(create_bulk_invitations)) .route("/api/admin/invitations/:id", get(get_invitation).delete(cancel_invitation)) .route("/api/admin/invitations/:id/resend", post(resend_invitation)) } pub async fn get_admin_dashboard( State(_state): State>, ) -> Html { let html = r##"

Quick Actions

System Health

API Server Healthy
99.9%
Uptime
Database Healthy
12ms
Avg Response
Storage Healthy
45%
Capacity Used
"##; Html(html.to_string()) } pub async fn get_stats_users( State(_state): State>, ) -> Html { let html = r##"
127 Total Users
"##; Html(html.to_string()) } pub async fn get_stats_groups( State(_state): State>, ) -> Html { let html = r##"
12 Active Groups
"##; Html(html.to_string()) } pub async fn get_stats_bots( State(_state): State>, ) -> Html { let html = r##"
8 Running Bots
"##; Html(html.to_string()) } pub async fn get_stats_storage( State(_state): State>, ) -> Html { let html = r##"
45.2 GB Storage Used
"##; Html(html.to_string()) } pub async fn get_admin_users( State(_state): State>, ) -> Html { let html = r##"
Name Email Role Status Actions
John Doe john@example.com Admin Active
Jane Smith jane@example.com User Active
"##; Html(html.to_string()) } pub async fn get_admin_groups( State(_state): State>, ) -> Html { let html = r##"
Name Members Created Actions
Engineering 15 2024-01-15
Marketing 8 2024-02-20
"##; Html(html.to_string()) } pub async fn get_admin_bots( State(_state): State>, ) -> Html { let html = r##"
Name Status Messages Last Active Actions
Support Bot Running 1,234 Just now
Sales Assistant Running 567 5 min ago
"##; Html(html.to_string()) } pub async fn get_admin_dns( State(_state): State>, ) -> Html { let html = r##"
Domain Type Status SSL Actions
bot.example.com CNAME Active Valid
"##; Html(html.to_string()) } pub async fn get_admin_billing( State(_state): State>, ) -> Html { let html = r##"

Current Plan

Enterprise
$499/month

Next Billing Date

January 15, 2025

Payment Method

**** **** **** 4242
"##; Html(html.to_string()) } pub async fn get_admin_audit( State(_state): State>, ) -> Html { let now = Utc::now(); let html = format!(r##"
Time User Action Details
{} admin@example.com User Login Successful login from 192.168.1.1
{} admin@example.com Settings Changed Updated system configuration
"##, now.format("%Y-%m-%d %H:%M"), now.format("%Y-%m-%d %H:%M")); Html(html) } pub async fn get_system_status( State(_state): State>, ) -> Result, (StatusCode, Json)> { let now = Utc::now(); let status = SystemStatusResponse { status: "healthy".to_string(), uptime_seconds: 3600 * 24 * 7, version: "1.0.0".to_string(), services: vec![ ServiceStatus { name: "ui_server".to_string(), status: "running".to_string(), uptime_seconds: 3600 * 24 * 7, memory_mb: 256.5, cpu_percent: 12.3, }, ServiceStatus { name: "database".to_string(), status: "running".to_string(), uptime_seconds: 3600 * 24 * 7, memory_mb: 512.8, cpu_percent: 8.5, }, ServiceStatus { name: "cache".to_string(), status: "running".to_string(), uptime_seconds: 3600 * 24 * 7, memory_mb: 128.2, cpu_percent: 3.2, }, ServiceStatus { name: "storage".to_string(), status: "running".to_string(), uptime_seconds: 3600 * 24 * 7, memory_mb: 64.1, cpu_percent: 5.8, }, ], health_checks: vec![ HealthCheck { name: "database_connection".to_string(), status: "passed".to_string(), message: Some("Connected successfully".to_string()), last_check: now, }, HealthCheck { name: "storage_access".to_string(), status: "passed".to_string(), message: Some("Storage accessible".to_string()), last_check: now, }, HealthCheck { name: "api_endpoints".to_string(), status: "passed".to_string(), message: Some("All endpoints responding".to_string()), last_check: now, }, ], last_restart: now.checked_sub_signed(chrono::Duration::days(7)).unwrap_or(now), }; Ok(Json(status)) } pub async fn get_system_metrics( State(_state): State>, ) -> Result, (StatusCode, Json)> { let metrics = SystemMetricsResponse { cpu_usage: 23.5, memory_total_mb: 8192, memory_used_mb: 4096, memory_percent: 50.0, disk_total_gb: 500, disk_used_gb: 350, disk_percent: 70.0, network_in_mbps: 12.5, network_out_mbps: 8.3, active_connections: 256, request_rate_per_minute: 1250, error_rate_percent: 0.5, }; Ok(Json(metrics)) } pub fn view_logs( State(_state): State>, Query(_params): Query, ) -> Result>, (StatusCode, Json)> { let now = Utc::now(); let logs = vec![ LogEntry { id: Uuid::new_v4(), timestamp: now, level: "info".to_string(), service: "ui_server".to_string(), message: "Request processed successfully".to_string(), metadata: Some(serde_json::json!({ "endpoint": "/api/files/list", "duration_ms": 45, "status_code": 200 })), }, LogEntry { id: Uuid::new_v4(), timestamp: now .checked_sub_signed(chrono::Duration::minutes(5)) .unwrap_or(now), level: "warning".to_string(), service: "database".to_string(), message: "Slow query detected".to_string(), metadata: Some(serde_json::json!({ "query": "SELECT * FROM users WHERE...", "duration_ms": 1250 })), }, LogEntry { id: Uuid::new_v4(), timestamp: now .checked_sub_signed(chrono::Duration::minutes(10)) .unwrap_or(now), level: "error".to_string(), service: "storage".to_string(), message: "Failed to upload file".to_string(), metadata: Some(serde_json::json!({ "file": "document.pdf", "error": "Connection timeout" })), }, ]; Ok(Json(logs)) } pub fn export_logs( State(_state): State>, Query(_params): Query, ) -> Result, (StatusCode, Json)> { Ok(Json(SuccessResponse { success: true, message: Some("Logs exported successfully".to_string()), })) } pub fn get_config( State(_state): State>, ) -> Result, (StatusCode, Json)> { let now = Utc::now(); let config = ConfigResponse { configs: vec![ ConfigItem { key: "max_upload_size_mb".to_string(), value: serde_json::json!(100), description: Some("Maximum file upload size in MB".to_string()), editable: true, requires_restart: false, }, ConfigItem { key: "session_timeout_minutes".to_string(), value: serde_json::json!(30), description: Some("User session timeout in minutes".to_string()), editable: true, requires_restart: false, }, ConfigItem { key: "enable_2fa".to_string(), value: serde_json::json!(true), description: Some("Enable two-factor authentication".to_string()), editable: true, requires_restart: false, }, ConfigItem { key: "database_pool_size".to_string(), value: serde_json::json!(20), description: Some("Database connection pool size".to_string()), editable: true, requires_restart: true, }, ], last_updated: now, }; Ok(Json(config)) } pub fn update_config( State(_state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { Ok(Json(SuccessResponse { success: true, message: Some(format!( "Configuration '{}' updated successfully", req.config_key )), })) } pub fn schedule_maintenance( State(_state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let maintenance_id = Uuid::new_v4(); let maintenance = MaintenanceResponse { id: maintenance_id, scheduled_at: req.scheduled_at, duration_minutes: req.duration_minutes, reason: req.reason, status: "scheduled".to_string(), created_by: "admin".to_string(), }; Ok(Json(maintenance)) } pub fn create_backup( State(_state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let backup_id = Uuid::new_v4(); let now = Utc::now(); let backup = BackupResponse { id: backup_id, backup_type: req.backup_type, size_bytes: 1024 * 1024 * 500, created_at: now, status: "completed".to_string(), download_url: Some(format!("/admin/backups/{}/download", backup_id)), expires_at: Some(now.checked_add_signed(chrono::Duration::days(30)).unwrap_or(now)), }; Ok(Json(backup)) } pub fn restore_backup( State(_state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { Ok(Json(SuccessResponse { success: true, message: Some(format!("Restore from backup {} initiated", req.backup_id)), })) } pub fn list_backups( State(_state): State>, ) -> Result>, (StatusCode, Json)> { let now = Utc::now(); let backups = vec![ BackupResponse { id: Uuid::new_v4(), backup_type: "full".to_string(), size_bytes: 1024 * 1024 * 500, created_at: now.checked_sub_signed(chrono::Duration::days(1)).unwrap_or(now), status: "completed".to_string(), download_url: Some("/admin/backups/1/download".to_string()), expires_at: Some(now.checked_add_signed(chrono::Duration::days(29)).unwrap_or(now)), }, BackupResponse { id: Uuid::new_v4(), backup_type: "incremental".to_string(), size_bytes: 1024 * 1024 * 50, created_at: now.checked_sub_signed(chrono::Duration::hours(12)).unwrap_or(now), status: "completed".to_string(), download_url: Some("/admin/backups/2/download".to_string()), expires_at: Some(now.checked_add_signed(chrono::Duration::days(29)).unwrap_or(now)), }, ]; Ok(Json(backups)) } pub fn manage_users( State(_state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let message = match req.action.as_str() { "suspend" => format!("User {} suspended", req.user_id), "activate" => format!("User {} activated", req.user_id), "delete" => format!("User {} deleted", req.user_id), "reset_password" => format!("Password reset for user {}", req.user_id), _ => format!("Action {} performed on user {}", req.action, req.user_id), }; Ok(Json(SuccessResponse { success: true, message: Some(message), })) } pub fn get_roles( State(_state): State>, ) -> Result>, (StatusCode, Json)> { let roles = vec![ serde_json::json!({ "id": Uuid::new_v4(), "name": "admin", "description": "Full system access", "permissions": ["*"], "user_count": 5 }), serde_json::json!({ "id": Uuid::new_v4(), "name": "user", "description": "Standard user access", "permissions": ["read:own", "write:own"], "user_count": 1245 }), serde_json::json!({ "id": Uuid::new_v4(), "name": "guest", "description": "Limited read-only access", "permissions": ["read:public"], "user_count": 328 }), ]; Ok(Json(roles)) } pub fn manage_roles( State(_state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { Ok(Json(SuccessResponse { success: true, message: Some(format!("Role '{}' managed successfully", req.role_name)), })) } pub fn get_quotas( State(_state): State>, ) -> Result>, (StatusCode, Json)> { let quotas = vec![ QuotaResponse { id: Uuid::new_v4(), entity_type: "user".to_string(), entity_id: Uuid::new_v4(), quota_type: "storage".to_string(), limit_value: 10 * 1024 * 1024 * 1024, current_value: 7 * 1024 * 1024 * 1024, percent_used: 70.0, }, QuotaResponse { id: Uuid::new_v4(), entity_type: "user".to_string(), entity_id: Uuid::new_v4(), quota_type: "api_calls".to_string(), limit_value: 10000, current_value: 3500, percent_used: 35.0, }, ]; Ok(Json(quotas)) } pub fn manage_quotas( State(_state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { Ok(Json(SuccessResponse { success: true, message: Some(format!("Quota '{}' set successfully", req.quota_type)), })) } pub fn get_licenses( State(_state): State>, ) -> Result>, (StatusCode, Json)> { let now = Utc::now(); let licenses = vec![LicenseResponse { id: Uuid::new_v4(), license_type: "enterprise".to_string(), status: "active".to_string(), max_users: 1000, current_users: 850, features: vec![ "unlimited_storage".to_string(), "advanced_analytics".to_string(), "priority_support".to_string(), "custom_integrations".to_string(), ], issued_at: now.checked_sub_signed(chrono::Duration::days(180)).unwrap_or(now), expires_at: Some(now.checked_add_signed(chrono::Duration::days(185)).unwrap_or(now)), }]; Ok(Json(licenses)) } pub fn manage_licenses( State(_state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { Ok(Json(SuccessResponse { success: true, message: Some(format!( "License '{}' activated successfully", req.license_type )), })) } // ============================================================================= // INVITATION MANAGEMENT HANDLERS // ============================================================================= /// List all invitations for the organization pub async fn list_invitations( State(state): State>, user: AuthenticatedUser, ) -> impl axum::response::IntoResponse { let mut conn = match state.conn.get() { Ok(c) => c, Err(e) => { return Json(serde_json::json!({ "success": false, "error": format!("Database connection error: {}", e), "invitations": [] })); } }; let org_id = user.organization_id.unwrap_or_else(Uuid::nil); let result: Result, _> = diesel::sql_query( "SELECT id, org_id, email, role, status, message, invited_by, created_at, expires_at, accepted_at FROM organization_invitations WHERE org_id = $1 ORDER BY created_at DESC LIMIT 100" ) .bind::(org_id) .load(&mut conn); match result { Ok(invitations) => Json(serde_json::json!({ "success": true, "invitations": invitations })), Err(e) => { warn!("Failed to list invitations: {}", e); // Return empty list on database error Json(serde_json::json!({ "success": false, "error": format!("Failed to fetch invitations: {}", e), "invitations": [] })) } } } /// Create a single invitation pub async fn create_invitation( State(state): State>, user: AuthenticatedUser, Json(payload): Json, ) -> impl axum::response::IntoResponse { // Validate email format if !payload.email.contains('@') { return (StatusCode::BAD_REQUEST, Json(InvitationResponse { success: false, id: None, email: Some(payload.email), error: Some("Invalid email format".to_string()), })); } let mut conn = match state.conn.get() { Ok(c) => c, Err(e) => { return (StatusCode::INTERNAL_SERVER_ERROR, Json(InvitationResponse { success: false, id: None, email: Some(payload.email), error: Some(format!("Database connection error: {}", e)), })); } }; let new_id = Uuid::new_v4(); let org_id = user.organization_id.unwrap_or_else(Uuid::nil); let invited_by = user.user_id; let expires_at = Utc::now() + chrono::Duration::days(7); let result = diesel::sql_query( "INSERT INTO organization_invitations (id, org_id, email, role, status, message, invited_by, created_at, expires_at) VALUES ($1, $2, $3, $4, 'pending', $5, $6, NOW(), $7) ON CONFLICT (org_id, email) WHERE status = 'pending' DO UPDATE SET role = EXCLUDED.role, message = EXCLUDED.message, expires_at = EXCLUDED.expires_at, updated_at = NOW() RETURNING id" ) .bind::(new_id) .bind::(org_id) .bind::(&payload.email) .bind::(&payload.role) .bind::, _>(payload.message.as_deref()) .bind::(invited_by) .bind::(expires_at) .execute(&mut conn); match result { Ok(_) => { // Send invitation email let email_to = payload.email.clone(); let invite_role = payload.role.clone(); let invite_message = payload.message.clone(); let invite_id = new_id; tokio::spawn(async move { if let Err(e) = send_invitation_email(&email_to, &invite_role, invite_message.as_deref(), invite_id).await { warn!("Failed to send invitation email to {}: {}", email_to, e); } }); (StatusCode::OK, Json(InvitationResponse { success: true, id: Some(new_id), email: Some(payload.email), error: None, })) } Err(e) => { warn!("Failed to create invitation: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(InvitationResponse { success: false, id: None, email: Some(payload.email), error: Some(format!("Failed to create invitation: {}", e)), })) } } } /// Create bulk invitations pub async fn create_bulk_invitations( State(state): State>, user: AuthenticatedUser, Json(payload): Json, ) -> impl axum::response::IntoResponse { let mut conn = match state.conn.get() { Ok(c) => c, Err(e) => { return Json(BulkInvitationResponse { success: false, sent: 0, failed: payload.emails.len() as i32, errors: vec![format!("Database connection error: {}", e)], }); } }; let org_id = user.organization_id.unwrap_or_else(Uuid::nil); let invited_by = user.user_id; let expires_at = Utc::now() + chrono::Duration::days(7); let mut sent = 0; let mut failed = 0; let mut errors = Vec::new(); for email in &payload.emails { // Validate email if !email.contains('@') { failed += 1; errors.push(format!("Invalid email: {}", email)); continue; } let new_id = Uuid::new_v4(); let result = diesel::sql_query( "INSERT INTO organization_invitations (id, org_id, email, role, status, message, invited_by, created_at, expires_at) VALUES ($1, $2, $3, $4, 'pending', $5, $6, NOW(), $7) ON CONFLICT (org_id, email) WHERE status = 'pending' DO NOTHING" ) .bind::(new_id) .bind::(org_id) .bind::(email) .bind::(&payload.role) .bind::, _>(payload.message.as_deref()) .bind::(invited_by) .bind::(expires_at) .execute(&mut conn); match result { Ok(_) => sent += 1, Err(e) => { failed += 1; errors.push(format!("Failed for {}: {}", email, e)); } } } Json(BulkInvitationResponse { success: failed == 0, sent, failed, errors, }) } /// Get a specific invitation pub async fn get_invitation( State(state): State>, user: AuthenticatedUser, Path(id): Path, ) -> impl axum::response::IntoResponse { let mut conn = match state.conn.get() { Ok(c) => c, Err(e) => { return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "success": false, "error": format!("Database connection error: {}", e) }))); } }; let org_id = user.organization_id.unwrap_or_else(Uuid::nil); let result: Result = diesel::sql_query( "SELECT id, org_id, email, role, status, message, invited_by, created_at, expires_at, accepted_at FROM organization_invitations WHERE id = $1 AND org_id = $2" ) .bind::(id) .bind::(org_id) .get_result(&mut conn); match result { Ok(invitation) => (StatusCode::OK, Json(serde_json::json!({ "success": true, "invitation": invitation }))), Err(_) => (StatusCode::NOT_FOUND, Json(serde_json::json!({ "success": false, "error": "Invitation not found" }))) } } /// Cancel/delete an invitation pub async fn cancel_invitation( State(state): State>, user: AuthenticatedUser, Path(id): Path, ) -> impl axum::response::IntoResponse { let mut conn = match state.conn.get() { Ok(c) => c, Err(e) => { return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "success": false, "error": format!("Database connection error: {}", e) }))); } }; let org_id = user.organization_id.unwrap_or_else(Uuid::nil); let result = diesel::sql_query( "UPDATE organization_invitations SET status = 'cancelled', updated_at = NOW() WHERE id = $1 AND org_id = $2 AND status = 'pending'" ) .bind::(id) .bind::(org_id) .execute(&mut conn); match result { Ok(rows) if rows > 0 => (StatusCode::OK, Json(serde_json::json!({ "success": true, "id": id }))), Ok(_) => (StatusCode::NOT_FOUND, Json(serde_json::json!({ "success": false, "error": "Invitation not found or already processed" }))), Err(e) => { warn!("Failed to cancel invitation: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "success": false, "error": format!("Failed to cancel invitation: {}", e) }))) } } } /// Resend an invitation email pub async fn resend_invitation( State(state): State>, user: AuthenticatedUser, Path(id): Path, ) -> impl axum::response::IntoResponse { let mut conn = match state.conn.get() { Ok(c) => c, Err(e) => { return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "success": false, "error": format!("Database connection error: {}", e) }))); } }; let org_id = user.organization_id.unwrap_or_else(Uuid::nil); let new_expires_at = Utc::now() + chrono::Duration::days(7); // Update expiration and resend let result = diesel::sql_query( "UPDATE organization_invitations SET expires_at = $3, updated_at = NOW() WHERE id = $1 AND org_id = $2 AND status = 'pending' RETURNING email" ) .bind::(id) .bind::(org_id) .bind::(new_expires_at) .execute(&mut conn); match result { Ok(rows) if rows > 0 => { // Trigger email resend let resend_id = id; tokio::spawn(async move { if let Err(e) = send_invitation_email_by_id(resend_id).await { warn!("Failed to resend invitation email for {}: {}", resend_id, e); } }); (StatusCode::OK, Json(serde_json::json!({ "success": true, "id": id, "message": "Invitation resent successfully" }))) } Ok(_) => (StatusCode::NOT_FOUND, Json(serde_json::json!({ "success": false, "error": "Invitation not found or not in pending status" }))), Err(e) => { warn!("Failed to resend invitation: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "success": false, "error": format!("Failed to resend invitation: {}", e) }))) } } } #[derive(Deserialize)] pub struct CreateGroupRequest { pub name: String, pub description: Option, } pub async fn create_group( State(state): State>, Json(req): Json, ) -> (StatusCode, Json) { let pool = &state.conn; let mut conn = match pool.get() { Ok(c) => c, Err(e) => { return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "success": false, "error": format!("Database connection error: {}", e) }))); } }; let group_id = Uuid::new_v4(); let result = diesel::sql_query( "INSERT INTO groups (id, name, description, created_at, updated_at) VALUES ($1, $2, $3, NOW(), NOW()) RETURNING id" ) .bind::(group_id) .bind::(&req.name) .bind::, _>(req.description.as_deref()) .execute(&mut conn); match result { Ok(_) => (StatusCode::CREATED, Json(serde_json::json!({ "success": true, "id": group_id, "name": req.name }))), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "success": false, "error": format!("Failed to create group: {}", e) }))) } } pub async fn export_admin_report( State(_state): State>, ) -> (StatusCode, Json) { (StatusCode::OK, Json(serde_json::json!({ "success": true, "report_url": "/api/admin/reports/latest.pdf", "generated_at": Utc::now().to_rfc3339() }))) } pub async fn get_dashboard_stats( State(_state): State>, ) -> Html { Html(r##"
24Team Members
+3 this month
5Active Bots
All operational
12.4KMessages Today
+18% vs yesterday
45.2 GBStorage Used
of 100 GB
"##.to_string()) } pub async fn get_dashboard_health( State(_state): State>, ) -> Html { Html(r##"
API ServerOperational
DatabaseOperational
Bot EngineOperational
File StorageOperational
"##.to_string()) } pub async fn get_dashboard_activity( State(_state): State>, Query(params): Query>, ) -> Html { let _page = params.get("page").and_then(|p| p.parse::().ok()).unwrap_or(1); Html(r##"
John Doe joined the organization
2 hours ago
Support Bot processed 150 messages
3 hours ago
System security scan completed
5 hours ago
"##.to_string()) } pub async fn get_dashboard_members( State(_state): State>, ) -> Html { Html(r##"
JD
John DoeAdmin
Online
JS
Jane SmithMember
Online
BW
Bob WilsonMember
Offline
"##.to_string()) } pub async fn get_dashboard_roles( State(_state): State>, ) -> Html { Html(r##"
Owner1
Admin3
Member18
Guest2
"##.to_string()) } pub async fn get_dashboard_bots( State(_state): State>, ) -> Html { Html(r##"
CS
Customer Support BotHandles customer inquiries
Active
SA
Sales AssistantLead qualification
Active
HR
HR HelperEmployee onboarding
Paused
"##.to_string()) } pub async fn get_dashboard_invitations( State(_state): State>, ) -> Html { Html(r##"
alice@example.comMember
Pending Expires in 5 days
bob@example.comAdmin
Pending Expires in 3 days
"##.to_string()) }