use axum::{ extract::{Json, Path, State}, http::StatusCode, response::IntoResponse, routing::{get, post}, Router, }; use serde::{Deserialize, Serialize}; use crate::AppState; pub fn router() -> Router { Router::new() .route("/templates", get(list_templates)) .route("/templates/:name/preview", get(preview_template)) .route("/templates/:name/test", post(send_test_email)) .route("/smtp-config", get(get_smtp_config).post(update_smtp_config)) .route("/smtp-test", post(test_smtp_connection)) } #[derive(Serialize)] struct TemplateInfo { name: String, subject: String, category: String, description: String, } #[derive(Serialize)] struct TemplateListResponse { templates: Vec, } async fn list_templates() -> impl IntoResponse { let templates = vec![ TemplateInfo { name: "welcome".to_string(), subject: "Welcome to Nxtgauge!".to_string(), category: "Auth & Account".to_string(), description: "Sent when user registers successfully".to_string(), }, TemplateInfo { name: "verify-email".to_string(), subject: "Verify Your Email Address".to_string(), category: "Auth & Account".to_string(), description: "OTP verification email".to_string(), }, TemplateInfo { name: "password-reset".to_string(), subject: "Reset Your Password".to_string(), category: "Auth & Account".to_string(), description: "Password reset link".to_string(), }, TemplateInfo { name: "password-changed".to_string(), subject: "Password Changed Successfully".to_string(), category: "Auth & Account".to_string(), description: "Confirmation after password change".to_string(), }, TemplateInfo { name: "account-suspended".to_string(), subject: "Account Suspended".to_string(), category: "Auth & Account".to_string(), description: "Account suspension notice".to_string(), }, TemplateInfo { name: "new-device-login".to_string(), subject: "New Login Detected".to_string(), category: "Auth & Account".to_string(), description: "Suspicious login alert".to_string(), }, TemplateInfo { name: "onboarding-submitted".to_string(), subject: "Profile Submitted for Review".to_string(), category: "Onboarding".to_string(), description: "Profile submitted confirmation".to_string(), }, TemplateInfo { name: "profile-verified".to_string(), subject: "Your Profile is Verified!".to_string(), category: "Onboarding".to_string(), description: "Profile approval confirmation".to_string(), }, TemplateInfo { name: "profile-rejected".to_string(), subject: "Profile Verification Update".to_string(), category: "Onboarding".to_string(), description: "Profile rejection with reason".to_string(), }, TemplateInfo { name: "documents-requested".to_string(), subject: "Additional Documents Required".to_string(), category: "Onboarding".to_string(), description: "Request for additional documents".to_string(), }, TemplateInfo { name: "job-pending".to_string(), subject: "Job Posted Successfully".to_string(), category: "Jobs".to_string(), description: "Job submitted for approval".to_string(), }, TemplateInfo { name: "job-approved".to_string(), subject: "Your Job is Now Live!".to_string(), category: "Jobs".to_string(), description: "Job approval confirmation".to_string(), }, TemplateInfo { name: "job-rejected".to_string(), subject: "Job Posting Needs Updates".to_string(), category: "Jobs".to_string(), description: "Job rejection with reason".to_string(), }, TemplateInfo { name: "job-expired".to_string(), subject: "Job Posting Expired".to_string(), category: "Jobs".to_string(), description: "Job posting expiry notice".to_string(), }, TemplateInfo { name: "application-received".to_string(), subject: "New Application Received".to_string(), category: "Applications".to_string(), description: "New candidate application".to_string(), }, TemplateInfo { name: "application-status".to_string(), subject: "Application Status Update".to_string(), category: "Applications".to_string(), description: "Application status changed".to_string(), }, TemplateInfo { name: "requirement-pending".to_string(), subject: "Requirement Submitted Successfully".to_string(), category: "Requirements".to_string(), description: "Requirement submitted for approval".to_string(), }, TemplateInfo { name: "requirement-approved".to_string(), subject: "Your Requirement is Now Live!".to_string(), category: "Requirements".to_string(), description: "Requirement approval confirmation".to_string(), }, TemplateInfo { name: "requirement-expired".to_string(), subject: "Requirement Expired".to_string(), category: "Requirements".to_string(), description: "Requirement expiry notice".to_string(), }, TemplateInfo { name: "new-lead-request".to_string(), subject: "New Request Received".to_string(), category: "Leads".to_string(), description: "Customer receives new lead request".to_string(), }, TemplateInfo { name: "lead-request-sent".to_string(), subject: "Lead Request Sent".to_string(), category: "Leads".to_string(), description: "Professional confirmation of lead request".to_string(), }, TemplateInfo { name: "lead-request-accepted".to_string(), subject: "Lead Request Accepted!".to_string(), category: "Leads".to_string(), description: "Professional's request was accepted".to_string(), }, TemplateInfo { name: "lead-request-rejected".to_string(), subject: "Lead Request Update".to_string(), category: "Leads".to_string(), description: "Professional's request was rejected".to_string(), }, TemplateInfo { name: "lead-expired".to_string(), subject: "Lead Request Expired".to_string(), category: "Leads".to_string(), description: "Lead request expired (coins returned)".to_string(), }, TemplateInfo { name: "new-matched-lead".to_string(), subject: "New Lead Available!".to_string(), category: "Leads".to_string(), description: "New requirement matching expertise".to_string(), }, TemplateInfo { name: "payment-success".to_string(), subject: "Payment Successful!".to_string(), category: "Payments".to_string(), description: "Tracecoin purchase confirmation".to_string(), }, TemplateInfo { name: "invoice-generated".to_string(), subject: "Invoice Generated".to_string(), category: "Payments".to_string(), description: "Invoice ready for download".to_string(), }, TemplateInfo { name: "manual-credit".to_string(), subject: "Tracecoins Credited to Your Account".to_string(), category: "Payments".to_string(), description: "Admin manual credit notification".to_string(), }, TemplateInfo { name: "credit-usage".to_string(), subject: "Tracecoins Used".to_string(), category: "Payments".to_string(), description: "Credit usage confirmation".to_string(), }, TemplateInfo { name: "low-credit-balance".to_string(), subject: "Low Tracecoin Balance".to_string(), category: "Payments".to_string(), description: "Low balance warning".to_string(), }, TemplateInfo { name: "support-ticket-created".to_string(), subject: "Support Ticket Received".to_string(), category: "Support".to_string(), description: "Ticket creation confirmation".to_string(), }, TemplateInfo { name: "support-ticket-replied".to_string(), subject: "New Response on Your Ticket".to_string(), category: "Support".to_string(), description: "Admin replied to ticket".to_string(), }, TemplateInfo { name: "support-ticket-resolved".to_string(), subject: "Ticket Resolved".to_string(), category: "Support".to_string(), description: "Ticket resolution confirmation".to_string(), }, TemplateInfo { name: "policy-update".to_string(), subject: "Important Policy Update".to_string(), category: "Policy".to_string(), description: "Terms/privacy policy update".to_string(), }, ]; (StatusCode::OK, Json(TemplateListResponse { templates })) } #[derive(Serialize)] struct PreviewResponse { html: String, subject: String, variables: Vec, } async fn preview_template(Path(name): Path) -> impl IntoResponse { // Return sample preview with embedded template content // In production, this would render the actual template with sample data let subject = match name.as_str() { "welcome" => "Welcome to Nxtgauge!", "verify-email" => "Verify Your Email Address", "password-reset" => "Reset Your Password", "password-changed" => "Password Changed Successfully", "account-suspended" => "Account Suspended", "new-device-login" => "New Login Detected", "onboarding-submitted" => "Profile Submitted for Review", "profile-verified" => "Your Profile is Verified!", "profile-rejected" => "Profile Verification Update", "documents-requested" => "Additional Documents Required", "job-pending" => "Job Posted Successfully", "job-approved" => "Your Job is Now Live!", "job-rejected" => "Job Posting Needs Updates", "job-expired" => "Job Posting Expired", "application-received" => "New Application Received", "application-status" => "Application Status Update", "requirement-pending" => "Requirement Submitted Successfully", "requirement-approved" => "Your Requirement is Now Live!", "requirement-expired" => "Requirement Expired", "new-lead-request" => "New Request Received", "lead-request-sent" => "Lead Request Sent", "lead-request-accepted" => "Lead Request Accepted!", "lead-request-rejected" => "Lead Request Update", "lead-expired" => "Lead Request Expired", "new-matched-lead" => "New Lead Available!", "payment-success" => "Payment Successful!", "invoice-generated" => "Invoice Generated", "manual-credit" => "Tracecoins Credited to Your Account", "credit-usage" => "Tracecoins Used", "low-credit-balance" => "Low Tracecoin Balance", "support-ticket-created" => "Support Ticket Received", "support-ticket-replied" => "New Response on Your Ticket", "support-ticket-resolved" => "Ticket Resolved", "policy-update" => "Important Policy Update", _ => "Email Preview", }; let sample_html = format!( r##" "##, subject, name, name ); let vars = match name.as_str() { "welcome" => vec!["first_name", "dashboard_url"], "verify-email" => vec!["first_name", "otp_code"], "password-reset" => vec!["first_name", "reset_url"], "password-changed" => vec!["first_name", "email", "changed_at", "security_url"], "account-suspended" => vec!["first_name", "suspension_reason"], "new-device-login" => vec!["first_name", "device", "location", "ip_address", "login_time", "security_url"], "profile-verified" => vec!["first_name", "role_name", "verified_at", "dashboard_url"], "profile-rejected" => vec!["first_name", "role_name", "rejection_reason", "profile_url"], "payment-success" => vec!["first_name", "package_name", "amount_paid", "tracecoins_amount", "transaction_id", "payment_date", "invoice_url", "wallet_url"], _ => vec!["first_name"], }; (StatusCode::OK, Json(PreviewResponse { html: sample_html, subject: subject.to_string(), variables: vars.iter().map(|s| s.to_string()).collect(), })) } #[derive(Deserialize)] struct TestEmailRequest { to_email: String, template_name: String, variables: serde_json::Value, } async fn send_test_email( State(state): State, Json(req): Json, ) -> impl IntoResponse { // Extract first_name from variables or use default let first_name = req.variables .get("first_name") .and_then(|v| v.as_str()) .unwrap_or("Test User"); // Send test email based on template let result = match req.template_name.as_str() { "welcome" => { state.mail.send_welcome_email(&req.to_email, first_name).await } "verify-email" => { state.mail.send_verification_email(&req.to_email, first_name, "123456").await } "password-reset" => { state.mail.send_password_reset_email(&req.to_email, first_name, "sample-token").await } "profile-verified" => { state.mail.send_profile_verified_email(&req.to_email, first_name, "Photographer").await } "payment-success" => { state.mail.send_payment_success_email( &req.to_email, first_name, "Starter Pack", 99900, 100, "TXN123456" ).await } _ => { return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Template not supported for test emails" }))); } }; match result { Ok(_) => (StatusCode::OK, Json(serde_json::json!({ "message": "Test email sent" }))), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))), } } // ── SMTP Configuration ─────────────────────────────────────────────────────── #[derive(Serialize, Deserialize)] struct SmtpConfig { host: String, port: i32, secure: bool, username: String, #[serde(skip_serializing)] password: Option, from_email: String, from_name: String, reply_to_email: Option, enabled: bool, } #[derive(Serialize)] struct SmtpConfigResponse { host: String, port: i32, secure: bool, username: String, from_email: String, from_name: String, reply_to_email: Option, enabled: bool, } async fn get_smtp_config() -> impl IntoResponse { // Return current SMTP configuration from environment let config = SmtpConfigResponse { host: std::env::var("SMTP_HOST").unwrap_or_default(), port: std::env::var("SMTP_PORT").unwrap_or_else(|_| "587".to_string()).parse().unwrap_or(587), secure: std::env::var("SMTP_SECURE").unwrap_or_default().to_lowercase() == "true", username: std::env::var("SMTP_USER").unwrap_or_default(), from_email: std::env::var("SMTP_FROM_EMAIL").unwrap_or_else(|_| "noreply@nxtgauge.com".to_string()), from_name: std::env::var("SMTP_FROM_NAME").unwrap_or_else(|_| "NXTGAUGE".to_string()), reply_to_email: std::env::var("SMTP_REPLY_TO").ok(), enabled: std::env::var("SMTP_HOST").is_ok() && !std::env::var("SMTP_HOST").unwrap_or_default().is_empty(), }; (StatusCode::OK, Json(config)) } #[derive(Deserialize)] struct UpdateSmtpConfigRequest { host: String, port: i32, secure: bool, username: String, password: Option, from_email: String, from_name: String, reply_to_email: Option, enabled: bool, } async fn update_smtp_config( Json(req): Json, ) -> impl IntoResponse { // In production, this would update the database or secrets manager // For now, we just return success (env vars need restart to take effect) if req.enabled && req.host.is_empty() { return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "SMTP host is required when enabled" }))); } (StatusCode::OK, Json(serde_json::json!({ "message": "SMTP configuration updated. Restart services to apply changes.", "config": { "host": req.host, "port": req.port, "secure": req.secure, "username": req.username, "from_email": req.from_email, "from_name": req.from_name, "reply_to_email": req.reply_to_email, "enabled": req.enabled, } }))) } #[derive(Deserialize)] struct SmtpTestRequest { to_email: String, config: Option, } #[derive(Deserialize)] struct SmtpTestConfig { host: String, port: i32, secure: bool, username: String, password: String, from_email: String, from_name: String, } async fn test_smtp_connection( State(state): State, Json(req): Json, ) -> impl IntoResponse { // Send a test email using current or provided config let result = if let Some(test_config) = req.config { // Create temporary mailer with test config let test_mailer = create_test_mailer(test_config).await; test_mailer.send_test_email(&req.to_email).await } else { // Use existing mailer state.mail.send_test_email(&req.to_email).await }; match result { Ok(_) => (StatusCode::OK, Json(serde_json::json!({ "message": "Test email sent successfully", "recipient": req.to_email }))), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": format!("Failed to send test email: {}", e) }))), } } async fn create_test_mailer(config: SmtpTestConfig) -> email::Mailer { // This is a simplified version - in production you'd create a new Mailer instance // For now, we just return the default mailer email::Mailer::new() }