feat: implement core Go application with web server
- 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
This commit is contained in:
582
output/internal/api/handler.go
Normal file
582
output/internal/api/handler.go
Normal file
@@ -0,0 +1,582 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user