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:
YourDreamNameHere
2025-11-20 16:36:28 -05:00
parent aa93326897
commit 89443f213b
57 changed files with 14404 additions and 0 deletions

View 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
}