package api import ( "net/http" "regexp" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/ydn/yourdreamnamehere/internal/models" "github.com/ydn/yourdreamnamehere/internal/services" "gorm.io/gorm" ) type Handler struct { userService *services.UserService stripeService *services.StripeService ovhService *services.OVHService cloudronService *services.CloudronService dolibarrService *services.DolibarrService deploymentService *services.DeploymentService emailService *services.EmailService } func NewHandler( userService *services.UserService, stripeService *services.StripeService, ovhService *services.OVHService, cloudronService *services.CloudronService, dolibarrService *services.DolibarrService, deploymentService *services.DeploymentService, emailService *services.EmailService, ) *Handler { return &Handler{ userService: userService, stripeService: stripeService, ovhService: ovhService, cloudronService: cloudronService, dolibarrService: dolibarrService, deploymentService: deploymentService, emailService: emailService, } } func (h *Handler) RegisterRoutes(router *gin.Engine) { // Health check router.GET("/health", h.HealthCheck) // Public routes public := router.Group("/api/v1") { public.POST("/register", h.RegisterUser) public.POST("/login", h.LoginUser) public.GET("/pricing", h.GetPricing) public.POST("/checkout", h.CreateCheckoutSession) public.POST("/webhooks/stripe", h.StripeWebhook) } // Protected routes protected := router.Group("/api/v1") protected.Use(middleware.AuthMiddleware(cfg)) { // User routes (all authenticated users) protected.GET("/profile", h.GetUserProfile) protected.PUT("/profile", h.UpdateUserProfile) protected.GET("/domains", h.ListDomains) protected.POST("/domains", h.CreateDomain) protected.GET("/domains/:id", h.GetDomain) protected.GET("/vps", h.ListVPS) protected.GET("/vps/:id", h.GetVPS) protected.GET("/subscriptions", h.ListSubscriptions) protected.POST("/subscriptions/cancel", h.CancelSubscription) protected.GET("/deployments", h.ListDeploymentLogs) protected.GET("/invitations/:token", h.GetInvitation) protected.POST("/invitations/:token/accept", h.AcceptInvitation) // Admin routes (admin only) admin := protected.Group("/admin") admin.Use(middleware.RequireRole("admin")) { admin.GET("/users", h.ListUsers) admin.GET("/users/:id", h.GetUserDetails) admin.DELETE("/users/:id", h.DeleteUser) admin.GET("/system/status", h.GetSystemStatus) admin.POST("/system/backup", h.TriggerBackup) } } } func (h *Handler) HealthCheck(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "healthy", "service": "YourDreamNameHere API", "version": "1.0.0", }) } func (h *Handler) RegisterUser(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` FirstName string `json:"first_name" binding:"required"` LastName string `json:"last_name" binding:"required"` Password string `json:"password" binding:"required,min=8"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } user, err := h.userService.CreateUser(req.Email, req.FirstName, req.LastName, req.Password) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{ "message": "User created successfully", "user": user, }) } func (h *Handler) LoginUser(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } token, err := h.userService.AuthenticateUser(req.Email, req.Password) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) return } c.JSON(http.StatusOK, gin.H{ "token": token, "message": "Login successful", }) } func (h *Handler) GetUserProfile(c *gin.Context) { userID := c.GetString("user_id") user, err := h.userService.GetUserByID(userID) if err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user profile"}) return } c.JSON(http.StatusOK, gin.H{"user": user}) } func (h *Handler) UpdateUserProfile(c *gin.Context) { userID := c.GetString("user_id") var req struct { FirstName string `json:"first_name"` LastName string `json:"last_name"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } user, err := h.userService.UpdateUser(userID, req.FirstName, req.LastName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user profile"}) return } c.JSON(http.StatusOK, gin.H{ "message": "Profile updated successfully", "user": user, }) } func (h *Handler) GetPricing(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "plans": []gin.H{ { "name": "Sovereign Hosting", "price": 25000, // $250.00 in cents "currency": "usd", "interval": "month", "features": []string{ "Domain registration via OVH", "VPS provisioning", "Cloudron installation", "DNS configuration", "Email invite for Cloudron setup", "24/7 support", }, }, }, }) } func (h *Handler) CreateCheckoutSession(c *gin.Context) { var req struct { DomainName string `json:"domain_name" binding:"required"` Email string `json:"email" binding:"required,email"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } sessionURL, err := h.stripeService.CreateCheckoutSession(req.Email, req.DomainName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create checkout session"}) return } c.JSON(http.StatusOK, gin.H{"checkout_url": sessionURL}) } func (h *Handler) StripeWebhook(c *gin.Context) { body, err := gin.GetRequestData(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"}) return } event, err := h.stripeService.HandleWebhook(c.Request.Header.Get("Stripe-Signature"), body) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } switch event.Type { case "checkout.session.completed": h.deploymentService.ProcessSuccessfulPayment(event.Data) case "invoice.payment_failed": h.deploymentService.ProcessFailedPayment(event.Data) default: c.JSON(http.StatusOK, gin.H{"received": true}) return } c.JSON(http.StatusOK, gin.H{"received": true}) } func (h *Handler) ListDomains(c *gin.Context) { userID := c.GetString("user_id") domains, err := h.userService.GetUserDomains(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list domains"}) return } c.JSON(http.StatusOK, gin.H{"domains": domains}) } func (h *Handler) CreateDomain(c *gin.Context) { userID := c.GetString("user_id") var req struct { Name string `json:"name" binding:"required,min=3,max=63"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Additional domain validation if !isValidDomain(req.Name) { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid domain name format"}) return } domain, err := h.deploymentService.CreateDomain(userID, req.Name) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create domain: " + err.Error()}) return } c.JSON(http.StatusCreated, gin.H{ "message": "Domain creation initiated", "domain": domain, }) } func (h *Handler) GetDomain(c *gin.Context) { domainID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid domain ID"}) return } userID := c.GetString("user_id") domain, err := h.userService.GetDomainByID(userID, domainID) if err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Domain not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get domain"}) return } c.JSON(http.StatusOK, gin.H{"domain": domain}) } func (h *Handler) ListVPS(c *gin.Context) { userID := c.GetString("user_id") vpsList, err := h.userService.GetUserVPS(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list VPS"}) return } c.JSON(http.StatusOK, gin.H{"vps": vpsList}) } func (h *Handler) GetVPS(c *gin.Context) { vpsID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid VPS ID"}) return } userID := c.GetString("user_id") vps, err := h.userService.GetVPSByID(userID, vpsID) if err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "VPS not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get VPS"}) return } c.JSON(http.StatusOK, gin.H{"vps": vps}) } func (h *Handler) ListSubscriptions(c *gin.Context) { userID := c.GetString("user_id") subscriptions, err := h.userService.GetUserSubscriptions(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list subscriptions"}) return } c.JSON(http.StatusOK, gin.H{"subscriptions": subscriptions}) } func (h *Handler) CancelSubscription(c *gin.Context) { userID := c.GetString("user_id") var req struct { SubscriptionID string `json:"subscription_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err = h.stripeService.CancelSubscription(req.SubscriptionID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cancel subscription"}) return } c.JSON(http.StatusOK, gin.H{"message": "Subscription cancellation initiated"}) } func (h *Handler) ListDeploymentLogs(c *gin.Context) { userID := c.GetString("user_id") vpsIDStr := c.Query("vps_id") var vpsID *uuid.UUID if vpsIDStr != "" { if id, err := uuid.Parse(vpsIDStr); err == nil { vpsID = &id } } logs, err := h.userService.GetDeploymentLogs(userID, vpsID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list deployment logs"}) return } c.JSON(http.StatusOK, gin.H{"logs": logs}) } func (h *Handler) GetInvitation(c *gin.Context) { token := c.Param("token") invitation, err := h.userService.GetInvitationByToken(token) if err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Invitation not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get invitation"}) return } c.JSON(http.StatusOK, gin.H{"invitation": invitation}) } func (h *Handler) AcceptInvitation(c *gin.Context) { token := c.Param("token") var req struct { Password string `json:"password" binding:"required,min=8"` FirstName string `json:"first_name" binding:"required"` LastName string `json:"last_name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err := h.userService.AcceptInvitation(token, req.Password, req.FirstName, req.LastName) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Invitation accepted successfully"}) } // Admin endpoints func (h *Handler) ListUsers(c *gin.Context) { // Check if user is admin userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"}) return } var users []models.User if err := h.userService.db.Select("id, email, first_name, last_name, role, is_active, created_at, updated_at"). Find(&users).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list users"}) return } c.JSON(http.StatusOK, gin.H{"users": users}) } func (h *Handler) GetUserDetails(c *gin.Context) { userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"}) return } userID := c.Param("id") var user models.User if err := h.userService.db.Preload("Customers").Preload("Customers.Domains").Preload("Customers.Subscriptions"). Where("id = ?", userID).First(&user).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user details"}) return } c.JSON(http.StatusOK, gin.H{"user": user}) } func (h *Handler) DeleteUser(c *gin.Context) { userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"}) return } userID := c.Param("id") currentUserID := c.GetString("user_id") // Prevent admin from deleting themselves if userID == currentUserID { c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete your own account"}) return } // Soft delete user if err := h.userService.db.Delete(&models.User{}, "id = ?", userID).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"}) return } c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"}) } func (h *Handler) GetSystemStatus(c *gin.Context) { userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"}) return } // Get system statistics stats := gin.H{ "total_users": 0, "active_users": 0, "total_customers": 0, "active_subscriptions": 0, "total_domains": 0, "total_vps": 0, } // Count users h.userService.db.Model(&models.User{}).Count(&stats["total_users"]) h.userService.db.Model(&models.User{}).Where("is_active = ?", true).Count(&stats["active_users"]) h.userService.db.Model(&models.Customer{}).Count(&stats["total_customers"]) h.userService.db.Model(&models.Subscription{}).Where("status = ?", "active").Count(&stats["active_subscriptions"]) h.userService.db.Model(&models.Domain{}).Count(&stats["total_domains"]) h.userService.db.Model(&models.VPS{}).Count(&stats["total_vps"]) c.JSON(http.StatusOK, gin.H{ "status": "healthy", "timestamp": time.Now(), "statistics": stats, }) } func (h *Handler) TriggerBackup(c *gin.Context) { userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"}) return } // In a real implementation, this would trigger a database backup // For now, we'll just log it log.Printf("Manual backup triggered by admin user %s", c.GetString("user_id")) c.JSON(http.StatusOK, gin.H{ "message": "Backup triggered successfully", "timestamp": time.Now(), }) } // Helper function for domain validation func isValidDomain(domain string) bool { // Basic domain validation regex domainRegex := `^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$` re := regexp.MustCompile(domainRegex) // Check length if len(domain) < 3 || len(domain) > 63 { return false } // Check regex if !re.MatchString(domain) { return false } // Check if it doesn't start or end with hyphen if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") { return false } return true }