- Add Go modules with required dependencies (Gin, UUID, JWT, etc.) - Implement main web server with landing page endpoint - Add comprehensive API endpoints for health and status - Include proper error handling and request validation - Set up CORS middleware and security headers
582 lines
16 KiB
Go
582 lines
16 KiB
Go
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
|
|
} |