Browse Source

init codebase

lblt 3 months ago
commit
ad0b651ec4

+ 17 - 0
.env.sample

@@ -0,0 +1,17 @@
+SERVER_ADDRESS=:8080
+DATABASE_URL=byom.db
+OVH_ENDPOINT=ovh-eu
+OVH_APP_KEY=your_ovh_app_key
+OVH_APP_SECRET=your_ovh_app_secret
+STRIPE_KEY=sk_test_your_stripe_key
+STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
+
+# Admin credentials (change these in production)
+ADMIN_USERNAME=admin
+ADMIN_PASSWORD=admin
+
+# SMTP Configuration (if using email notifications)
+SMTP_HOST=smtp.example.com
+SMTP_PORT=587
+SMTP_USERNAME=noreply@yourdomain.com
+SMTP_PASSWORD=your_smtp_password

+ 112 - 0
README.md

@@ -0,0 +1,112 @@
+# BYOM-Onboard
+## Description
+BYOM-Onboard est un service de provisionnement automatisé qui gère :
+
+- L'inscription des utilisateurs
+- Le traitement des paiements via Stripe
+- Le déploiement automatique de machines virtuelles sur OVH
+
+## Prérequis
+
+- Go 1.21 ou supérieur
+- SQLite
+- Compte OVH avec accès API
+- Compte Stripe
+- Git
+
+## Installation
+```bash
+# Cloner le projet
+git clone git@git.linuxforward.com:byom/byom-onboard.git
+
+# Se placer dans le répertoire
+cd byom-onboard
+
+# Installer les dépendances
+go mod download
+```
+
+## Configuration
+Créer un fichier .env à la racine du projet :
+
+```env
+SERVER_ADDRESS=:8080
+DATABASE_URL=postgres://user:password@localhost:5432/byom_onboard
+OVH_ENDPOINT=ovh-eu
+OVH_APP_KEY=votre-app-key
+OVH_APP_SECRET=votre-app-secret
+STRIPE_KEY=votre-stripe-key
+```
+
+## Structure du Projet
+```bash
+byom-onboard/
+├── cmd/
+│   └── server/           # Point d'entrée de l'application
+├── internal/
+│   ├── config/          # Configuration
+│   ├── domain/          # Logique métier
+│   ├── handlers/        # Gestionnaires HTTP
+│   ├── middleware/      # Middleware HTTP
+│   ├── repository/      # Accès aux données
+│   └── services/        # Services métier
+└── pkg/                 # Packages publics réutilisables
+```
+
+## Lancement
+```bash
+# En développement
+go run cmd/server/main.go
+
+# En production
+go build -o byom-onboard cmd/server/main.go
+./byom-onboard
+```
+
+## API
+
+### Workflow
+```mermaid
+sequenceDiagram
+    participant U as User
+    participant API as Registration API
+    participant DB as Database
+    participant PS as Payment Service
+    participant Q as Message Queue
+    participant P as Provisioning Service
+    participant C as Cloud Provider
+
+    U->>API: Submit registration
+    API->>DB: Store registration details
+    API->>PS: Initialize payment
+    PS-->>U: Redirect to payment gateway
+    U->>PS: Complete payment
+    PS-->>API: Payment confirmation
+    API->>Q: Send provisioning request
+    Q->>P: Process request
+    P->>C: Create VM instance
+    C-->>P: VM creation confirmation
+    P->>DB: Update VM details
+    P-->>U: Send access credentials
+```
+
+### Endpoints
+
+- POST /api/register - Inscription utilisateur
+- POST /api/payment - Traitement du paiement
+- POST /api/provision - Provisionnement VPS
+- GET /api/status - État du service
+
+## Contribution
+
+1. Forker le projet
+2. Créer une branche (git checkout -b feature/nouvelle-fonctionnalite)
+3. Commiter les changements (git commit -am 'Ajout nouvelle fonctionnalité')
+4. Pousser la branche (git push origin feature/nouvelle-fonctionnalite)
+5. Créer une Pull Request
+
+## Licence
+Propriétaire - Tous droits réservés
+
+## Contact
+Pour toute question ou suggestion, merci de contacter l'équipe BYOM.

+ 170 - 0
cmd/api/main.go

@@ -0,0 +1,170 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	"github.com/alecthomas/kong"
+	"github.com/gin-gonic/gin"
+
+	"git.linuxforward.com/byom/byom-onboard/internal/common/jwt"
+	"git.linuxforward.com/byom/byom-onboard/internal/domain/billing"
+	"git.linuxforward.com/byom/byom-onboard/internal/domain/provisioning"
+	"git.linuxforward.com/byom/byom-onboard/internal/domain/register"
+	"git.linuxforward.com/byom/byom-onboard/internal/platform/config"
+	"git.linuxforward.com/byom/byom-onboard/internal/platform/database"
+	"git.linuxforward.com/byom/byom-onboard/internal/platform/mailer"
+	"git.linuxforward.com/byom/byom-onboard/internal/platform/middleware"
+)
+
+var (
+	// Build information - values will be set during build
+	version   = "dev"
+	commit    = "none"
+	buildDate = "unknown"
+)
+
+type CLI struct {
+	Config  string     `help:"Config file path" default:"config.yaml" type:"path"`
+	Debug   bool       `help:"Enable debug mode"`
+	Version VersionCmd `cmd:"" help:"Show version information"`
+	Serve   ServeCmd   `cmd:"" help:"Start the HTTP server"`
+}
+
+type VersionCmd struct{}
+
+func (v VersionCmd) Run() error {
+	fmt.Printf("Version:\t%s\n", version)
+	fmt.Printf("Commit:\t\t%s\n", commit)
+	fmt.Printf("Built:\t\t%s\n", buildDate)
+	return nil
+}
+
+// ServeCmd handles the serve command
+type ServeCmd struct {
+	Port int    `help:"Port to listen on" default:"8080"`
+	Host string `help:"Host to bind to" default:"0.0.0.0"`
+}
+
+func (s *ServeCmd) Run(cli *CLI) error {
+	// Load configuration
+	cfg, err := config.Load()
+	if err != nil {
+		return fmt.Errorf("failed to load configuration: %w", err)
+	}
+
+	// Initialize Gin
+	gin.SetMode(gin.ReleaseMode)
+	r := gin.New()
+	r.Use(gin.Logger())
+	r.Use(gin.Recovery())
+
+	if cli.Debug {
+		log.Printf("Debug mode enabled")
+		gin.SetMode(gin.DebugMode)
+		log.Printf("Using config file: %s", cli.Config)
+	}
+
+	// Initialize database
+	db, err := database.NewDatabase(cfg.Database.DSN)
+	if err != nil {
+		return fmt.Errorf("failed to initialize database: %w", err)
+	}
+
+	jwtClient := jwt.NewJWTClient([]byte(cfg.JWT.Secret))
+
+	// Initialize services
+	stripeClient := billing.NewStripeClient(cfg.Stripe.SecretKey, cfg.Stripe.WebhookSecret)
+	mailerClient := mailer.NewMailer(&cfg.Mailer)
+	ovhClient := provisioning.NewOvhClient(cfg.OVH.Endpoint, cfg.OVH.AppKey, cfg.OVH.AppSecret)
+	registrationService := register.NewRegistrationService(db, jwtClient)
+
+	// Initialize handlers
+	billingHandler := billing.NewHandler(stripeClient, db)
+	provisioningHandler := provisioning.NewHandler(ovhClient, db)
+	registerHandler := register.NewHandler(registrationService, mailerClient, db)
+
+	// Setup server
+	server := setupServer(r, billingHandler, provisioningHandler, registerHandler, cfg)
+	server.Addr = fmt.Sprintf(":%d", s.Port)
+
+	// Start server in a goroutine
+	go func() {
+		log.Printf("Starting server on port %d", s.Port)
+		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+			log.Fatalf("Failed to start server: %v", err)
+		}
+	}()
+
+	// Setup graceful shutdown
+	quit := make(chan os.Signal, 1)
+	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+	<-quit
+
+	log.Println("Shutting down server...")
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	if err := server.Shutdown(ctx); err != nil {
+		return fmt.Errorf("server forced to shutdown: %w", err)
+	}
+
+	log.Println("Server exited properly")
+	return nil
+}
+
+func setupServer(
+	r *gin.Engine,
+	billingHandler *billing.Handler,
+	provisioningHandler *provisioning.Handler,
+	registerHandler *register.Handler,
+	config *config.Config,
+) *http.Server {
+
+	api := r.Group("/api/v1")
+	{
+		// Public routes
+		api.POST("/register", registerHandler.Register)
+		api.GET("/validate-email", registerHandler.ValidateEmail)
+
+		// Payment routes
+		api.POST("/payment", billingHandler.CreatePayment)
+		api.POST("/payment/webhook", billingHandler.HandleWebhook)
+
+		// Admin routes
+		adminMiddleware := middleware.NewAdminMiddleware()
+		admin := api.Group("/admin", adminMiddleware.Middleware())
+		{
+			admin.GET("/vms", provisioningHandler.ListVPS)
+			admin.POST("/vms", provisioningHandler.AddVPS)
+			admin.GET("/provisioning/:id", provisioningHandler.GetVPSStatus)
+		}
+	}
+
+	return &http.Server{
+		Addr:    config.Server.Address,
+		Handler: r,
+	}
+}
+
+func main() {
+	cli := CLI{}
+	ctx := kong.Parse(&cli,
+		kong.Name("byom"),
+		kong.Description("BYOM - Onboarding service"),
+		kong.UsageOnError(),
+		kong.ConfigureHelp(kong.HelpOptions{
+			Compact: true,
+			Summary: true,
+		}),
+	)
+
+	err := ctx.Run(&cli)
+	ctx.FatalIfErrorf(err)
+}

+ 80 - 0
config.sample.yml

@@ -0,0 +1,80 @@
+# Server Configuration
+server:
+  address: "0.0.0.0:8080"
+  port: 8080
+  tls:
+    enabled: false
+    cert_file: ""
+    key_file: ""
+  timeouts:
+    read: "15s"
+    write: "15s"
+    idle: "60s"
+
+# Database Configuration
+database:
+  url: "byom.db"  # SQLite database file
+  max_open_conns: 25
+  max_idle_conns: 5
+  conn_max_lifetime: "1h"
+  conn_max_idle_time: "30m"
+
+# Stripe Configuration
+stripe:
+  secret_key: "sk_test_your_stripe_secret_key"
+  webhook_secret: "whsec_your_stripe_webhook_secret"
+  currency: "EUR"
+
+# OVH Configuration
+ovh:
+  endpoint: "ovh-eu"
+  app_key: "your_ovh_app_key"
+  app_secret: "your_ovh_app_secret"
+  consumer_key: ""  # Optional, if you're using consumer key authentication
+
+# Mailer Configuration
+mailer:
+  enabled: true
+  host: "smtp.example.com"
+  port: "587"
+  username: "noreply@yourdomain.com"
+  password: "your_smtp_password"
+  from: "BYOM Service <noreply@yourdomain.com>"
+  encryption: "tls"
+
+# Logging Configuration
+logging:
+  level: "info"  # debug, info, warn, error
+  format: "json"  # json or text
+  output: "stdout"  # stdout, file
+  file_path: "logs/byom.log"  # Only used if output is set to file
+
+# Security Configuration
+security:
+  allowed_origins:
+    - "http://localhost:3000"
+    - "https://yourdomain.com"
+  rate_limit:
+    enabled: true
+    requests: 100
+    duration: "1m"
+
+# Admin Configuration
+admin:
+  username: "admin"  # Change this in production
+  password: "admin"  # Change this in production
+
+# VPS Plans Configuration
+vps_plans:
+  basic:
+    name: "Basic"
+    specs: "1-2-40"  # CPU-RAM-Storage
+    max_instances: 100
+  pro:
+    name: "Pro"
+    specs: "2-4-80"
+    max_instances: 50
+  enterprise:
+    name: "Enterprise"
+    specs: "16-16-160"
+    max_instances: 25

+ 69 - 0
go.mod

@@ -0,0 +1,69 @@
+module git.linuxforward.com/byom/byom-onboard
+
+go 1.22.5
+
+require (
+	github.com/stripe/stripe-go/v81 v81.1.0
+	gorm.io/driver/sqlite v1.5.6
+)
+
+require (
+	github.com/alecthomas/kong v1.5.1 // indirect
+	github.com/bytedance/sonic v1.11.6 // indirect
+	github.com/bytedance/sonic/loader v0.1.1 // indirect
+	github.com/cloudwego/base64x v0.1.4 // indirect
+	github.com/cloudwego/iasm v0.2.0 // indirect
+	github.com/fsnotify/fsnotify v1.7.0 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-playground/universal-translator v0.18.1 // indirect
+	github.com/go-playground/validator/v10 v10.20.0 // indirect
+	github.com/goccy/go-json v0.10.2 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
+	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.7 // indirect
+	github.com/leodido/go-urn v1.4.0 // indirect
+	github.com/magiconair/properties v1.8.7 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-sqlite3 v1.14.24 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+	github.com/sagikazarmark/locafero v0.4.0 // indirect
+	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+	github.com/sourcegraph/conc v0.3.0 // indirect
+	github.com/spf13/afero v1.11.0 // indirect
+	github.com/spf13/cast v1.6.0 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/spf13/viper v1.19.0 // indirect
+	github.com/subosito/gotenv v1.6.0 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	github.com/ugorji/go/codec v1.2.12 // indirect
+	go.uber.org/atomic v1.9.0 // indirect
+	go.uber.org/multierr v1.9.0 // indirect
+	golang.org/x/arch v0.8.0 // indirect
+	golang.org/x/crypto v0.23.0 // indirect
+	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
+	golang.org/x/oauth2 v0.18.0 // indirect
+	golang.org/x/sys v0.20.0 // indirect
+	golang.org/x/text v0.15.0 // indirect
+	google.golang.org/appengine v1.6.8 // indirect
+	google.golang.org/protobuf v1.34.1 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
+
+require (
+	github.com/gin-gonic/gin v1.10.0
+	github.com/google/uuid v1.6.0
+	github.com/joho/godotenv v1.5.1
+	github.com/ovh/go-ovh v1.6.0
+	golang.org/x/net v0.25.0 // indirect
+	gorm.io/gorm v1.25.12
+)

+ 187 - 0
go.sum

@@ -0,0 +1,187 @@
+github.com/alecthomas/kong v1.5.1 h1:9quB93P2aNGXf5C1kWNei85vjBgITNJQA4dSwJQGCOY=
+github.com/alecthomas/kong v1.5.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
+github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
+github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
+github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
+github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stripe/stripe-go/v81 v81.1.0 h1:OlpGPO2vhS2raLR/NuvHKeRUZ57FTkdZBTcd5Hhoyos=
+github.com/stripe/stripe-go/v81 v81.1.0/go.mod h1:C/F4jlmnGNacvYtBp/LUHCvVUJEZffFQCobkzwY1WOo=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
+go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
+golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
+gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
+gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
+gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

+ 20 - 0
internal/common/errors/error.go

@@ -0,0 +1,20 @@
+package errors
+
+type ErrorResponse struct {
+	Code    string `json:"code"`
+	Message string `json:"message"`
+}
+
+// Define error types
+var (
+	ErrInvalidRequest = ErrorResponse{Code: "INVALID_REQUEST", Message: "Invalid request format"}
+	ErrNotFound       = ErrorResponse{Code: "NOT_FOUND", Message: "Resource not found"}
+	ErrServerError    = ErrorResponse{Code: "SERVER_ERROR", Message: "Internal server error"}
+	ErrNoVPSAvailable = ErrorResponse{Code: "NO_VPS_AVAILABLE", Message: "No VPS available"}
+)
+
+// Implement error method for ErrorResponse
+
+func (e ErrorResponse) Error() string {
+	return e.Message
+}

+ 101 - 0
internal/common/jwt/jwt.go

@@ -0,0 +1,101 @@
+package jwt
+
+import (
+	"crypto/rand"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"net/url"
+	"time"
+
+	"github.com/golang-jwt/jwt/v5"
+)
+
+// JWTClient handles JWT operations for email verification
+type JWTClient struct {
+	secret []byte
+}
+
+// EmailVerificationClaims contains claims specific to email verification tokens
+type EmailVerificationClaims struct {
+	Email string `json:"email"`
+	jwt.RegisteredClaims
+}
+
+func NewJWTClient(secret []byte) *JWTClient {
+	return &JWTClient{secret: secret}
+}
+
+// GenerateEmailVerificationToken creates a JWT token for email verification
+// The token includes the email in both the claims and subject for additional security
+// and expires after 24 hours
+func (j *JWTClient) GenerateEmailVerificationToken(email string) (string, error) {
+	uniqueID, err := generateUniqueID()
+	if err != nil {
+		return "", fmt.Errorf("failed to generate unique ID: %w", err)
+	}
+
+	claims := EmailVerificationClaims{
+		Email: email,
+		RegisteredClaims: jwt.RegisteredClaims{
+			ExpiresAt: jwt.NewNumericDate(time.Now().UTC().Add(24 * time.Hour)),
+			IssuedAt:  jwt.NewNumericDate(time.Now().UTC()),
+			NotBefore: jwt.NewNumericDate(time.Now().UTC()),
+			Subject:   email,
+			ID:        uniqueID,
+		},
+	}
+
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+	return token.SignedString(j.secret)
+}
+
+func (j *JWTClient) GenerateEmailVerificationURL(email, baseURL string) (string, error) {
+	token, err := j.GenerateEmailVerificationToken(email)
+	if err != nil {
+		return "", fmt.Errorf("failed to generate token: %w", err)
+	}
+
+	// Ensure the token is URL-safe
+	return fmt.Sprintf("%s?token=%s", baseURL, url.QueryEscape(token)), nil
+}
+
+func (j *JWTClient) VerifyEmailToken(tokenString string) (string, error) {
+	token, err := jwt.ParseWithClaims(
+		tokenString,
+		&EmailVerificationClaims{},
+		func(token *jwt.Token) (interface{}, error) {
+			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+				return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
+			}
+			return j.secret, nil
+		},
+	)
+
+	if err != nil {
+		return "", fmt.Errorf("invalid token: %w", err)
+	}
+
+	if !token.Valid {
+		return "", errors.New("token is not valid")
+	}
+
+	claims, ok := token.Claims.(*EmailVerificationClaims)
+	if !ok {
+		return "", errors.New("invalid claims type")
+	}
+
+	if claims.Subject != claims.Email {
+		return "", errors.New("token subject does not match email")
+	}
+
+	return claims.Email, nil
+}
+
+func generateUniqueID() (string, error) {
+	bytes := make([]byte, 16)
+	if _, err := rand.Read(bytes); err != nil {
+		return "", err
+	}
+	return hex.EncodeToString(bytes), nil
+}

+ 52 - 0
internal/common/models/common.go

@@ -0,0 +1,52 @@
+package models
+
+type PaymentIntent struct {
+	ID           string `json:"id"`
+	ClientSecret string `json:"client_secret"`
+	Amount       int64  `json:"amount"`
+	Currency     string `json:"currency"`
+	Status       string `json:"status"`
+}
+
+type PaymentRequest struct {
+	PaymentMethodID string `json:"paymentMethodId"`
+	Amount          int64  `json:"amount"`
+	Currency        string `json:"currency"`
+}
+
+type RegisterRequest struct {
+	Name    string `json:"name" binding:"required"`
+	Surname string `json:"surname" binding:"required"`
+	Email   string `json:"email" binding:"required,email"`
+}
+
+type ValidateEmailRequest struct {
+	Token string `json:"token" binding:"required"`
+}
+
+type User struct {
+	ID      string `json:"id"`
+	Name    string `json:"name"`
+	Surname string `json:"surname"`
+	Email   string `json:"email"`
+	Token   string `json:"token"`
+	Status  bool   `json:"status"`
+}
+
+type VPSInstance struct {
+	ID          string `json:"id"`
+	DisplayName string `json:"display_name"`
+	IpAddress   string `json:"ip_address"`
+	Plan        string `json:"plan"`
+	Status      string `json:"status"`
+}
+
+type AddVPSRequest struct {
+	DisplayName string `json:"display_name" binding:"required"`
+	IpAddress   string `json:"ip_address" binding:"required"`
+	Plan        string `json:"plan" binding:"required"`
+}
+
+type GetVPSStatusRequest struct {
+	ID string `json:"id" binding:"required"`
+}

+ 65 - 0
internal/common/models/plan.go

@@ -0,0 +1,65 @@
+package models
+
+type Plan struct {
+	ID          string  `json:"id"`
+	Name        string  `json:"name"`
+	Description string  `json:"description"`
+	Price       int64   `json:"price"`
+	Currency    string  `json:"currency"`
+	Specs       VMSpecs `json:"specs"`
+}
+
+type VMSpecs struct {
+	CPU     int    `json:"cpu"`
+	RAM     int    `json:"ram_gb"`     // RAM in GB
+	Storage int    `json:"storage_gb"` // Storage in GB
+	Region  string `json:"region"`
+}
+
+const (
+	PlanBasic      = "basic"
+	PlanPro        = "pro"
+	PlanEnterprise = "enterprise"
+)
+
+var DefaultPlans = map[string]*Plan{
+	PlanBasic: {
+		ID:          PlanBasic,
+		Name:        "Basic",
+		Description: "Perfect for starting projects",
+		Price:       999, // 9.99 EUR
+		Currency:    "EUR",
+		Specs: VMSpecs{
+			CPU:     1,
+			RAM:     2,
+			Storage: 40,
+		},
+	},
+	PlanPro: {
+		ID:       PlanPro,
+		Name:     "Professional",
+		Price:    2999, // 29.99 EUR
+		Currency: "EUR",
+		Specs: VMSpecs{
+			CPU:     2,
+			RAM:     4,
+			Storage: 80,
+		},
+	},
+	PlanEnterprise: {
+		ID:       PlanEnterprise,
+		Name:     "Enterprise",
+		Price:    9999, // 99.99 EUR
+		Currency: "EUR",
+		Specs: VMSpecs{
+			CPU:     4,
+			RAM:     16,
+			Storage: 160,
+		},
+	},
+}
+
+func GetPlan(planID string) (*Plan, bool) {
+	plan, exists := DefaultPlans[planID]
+	return plan, exists
+}

+ 81 - 0
internal/domain/billing/handler.go

@@ -0,0 +1,81 @@
+package billing
+
+import (
+	"io"
+	"net/http"
+
+	"git.linuxforward.com/byom/byom-onboard/internal/common/models"
+	"git.linuxforward.com/byom/byom-onboard/internal/platform/database"
+	"github.com/gin-gonic/gin"
+)
+
+type Handler struct {
+	client BillingClient
+	db     *database.Database
+}
+
+func NewHandler(client BillingClient, db *database.Database) *Handler {
+	return &Handler{
+		client: client,
+		db:     db,
+	}
+}
+
+func (h *Handler) CreatePayment(c *gin.Context) {
+	var req models.PaymentRequest
+	if err := c.ShouldBindJSON(&req); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
+		return
+	}
+
+	intent, err := h.client.CreatePaymentIntent(req.Amount, req.Currency, req.PaymentMethodID)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+		return
+	}
+
+	c.JSON(http.StatusOK, intent)
+}
+
+func (h *Handler) HandleWebhook(c *gin.Context) {
+	payload, err := io.ReadAll(c.Request.Body)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "Unable to read request body"})
+		return
+	}
+
+	signature := c.GetHeader("Stripe-Signature")
+	if err := h.client.HandleWebhook(payload, signature); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{"received": true})
+}
+
+func (h *Handler) InitiateSubscription(c *gin.Context) {
+	var req struct {
+		SessionToken string `json:"session_token" binding:"required"`
+		PlanID       string `json:"plan_id" binding:"required"`
+	}
+	if err := c.ShouldBindJSON(&req); err != nil {
+		c.JSON(400, gin.H{"error": "Invalid request"})
+		return
+	}
+
+	// Get plan details from DefaultPlans
+	plan, exists := models.GetPlan(req.PlanID)
+	if !exists {
+		c.JSON(400, gin.H{"error": "Invalid plan"})
+		return
+	}
+
+	// Create payment intent with plan price
+	intent, err := h.client.CreatePaymentIntent(plan.Price, plan.Currency, "")
+	if err != nil {
+		c.JSON(500, gin.H{"error": "Failed to create payment"})
+		return
+	}
+
+	c.JSON(200, intent)
+}

+ 18 - 0
internal/domain/billing/service.go

@@ -0,0 +1,18 @@
+package billing
+
+import (
+	"git.linuxforward.com/byom/byom-onboard/internal/common/models"
+)
+
+type BillingClient interface {
+	CreatePaymentIntent(amount int64, currency string, paymentMethodID string) (*models.PaymentIntent, error)
+	HandleWebhook(payload []byte, signature string) error
+}
+
+// TODO: Implement later
+// To check available plan and prices
+// Can also provide dynamic prices
+// type PlanService interface {
+// 	GetPlan(planID string) (*models.Plan, error)
+// 	ListPlans() ([]*models.Plan, error)
+// }

+ 96 - 0
internal/domain/billing/stripe.go

@@ -0,0 +1,96 @@
+package billing
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+
+	"git.linuxforward.com/byom/byom-onboard/internal/common/models"
+
+	"github.com/stripe/stripe-go/v81"
+	"github.com/stripe/stripe-go/v81/paymentintent"
+	"github.com/stripe/stripe-go/v81/webhook"
+)
+
+type StripeClient struct {
+	secretKey     string
+	webhookSecret string
+}
+
+func NewStripeClient(secretKey, webhookSecret string) *StripeClient {
+	stripe.Key = secretKey
+	return &StripeClient{
+		secretKey:     secretKey,
+		webhookSecret: webhookSecret,
+	}
+}
+
+func (s *StripeClient) CreatePaymentIntent(amount int64, currency string, paymentMethodID string) (*models.PaymentIntent, error) {
+	params := &stripe.PaymentIntentParams{
+		Amount:   stripe.Int64(amount),
+		Currency: stripe.String(currency),
+		AutomaticPaymentMethods: &stripe.PaymentIntentAutomaticPaymentMethodsParams{
+			AllowRedirects: stripe.String("never"),
+			Enabled:        stripe.Bool(true),
+		},
+	}
+
+	if paymentMethodID != "" {
+		params.PaymentMethod = stripe.String(paymentMethodID)
+		params.Confirm = stripe.Bool(true)
+	}
+
+	pi, err := paymentintent.New(params)
+	if err != nil {
+		return nil, fmt.Errorf("error creating payment intent: %w", err)
+	}
+
+	return &models.PaymentIntent{
+		ID:           pi.ID,
+		ClientSecret: pi.ClientSecret,
+		Amount:       pi.Amount,
+		Currency:     string(pi.Currency),
+		Status:       string(pi.Status),
+	}, nil
+}
+
+func (s *StripeClient) HandleWebhook(payload []byte, signature string) error {
+	event, err := webhook.ConstructEvent(payload, signature, s.webhookSecret)
+	if err != nil {
+		return fmt.Errorf("error verifying webhook signature: %w", err)
+	}
+
+	return s.processStripeEvent(event)
+}
+
+func (s *StripeClient) processStripeEvent(event stripe.Event) error {
+	switch event.Type {
+	case stripe.EventTypePaymentIntentSucceeded:
+		return s.handlePaymentIntentSucceeded(event)
+	case stripe.EventTypePaymentIntentPaymentFailed:
+		return s.handlePaymentIntentFailed(event)
+	// ... other event types
+	default:
+		log.Printf("Unhandled event type: %s", event.Type)
+		return nil
+	}
+}
+
+// Event handlers moved to separate methods
+func (s *StripeClient) handlePaymentIntentSucceeded(event stripe.Event) error {
+	var pi stripe.PaymentIntent
+	if err := json.Unmarshal(event.Data.Raw, &pi); err != nil {
+		return fmt.Errorf("error parsing PaymentIntent: %w", err)
+	}
+	log.Printf("PaymentIntent %s succeeded!", pi.ID)
+	return nil
+}
+
+func (s *StripeClient) handlePaymentIntentFailed(event stripe.Event) error {
+	var pi stripe.PaymentIntent
+	if err := json.Unmarshal(event.Data.Raw, &pi); err != nil {
+		return fmt.Errorf("error parsing PaymentIntent: %w", err)
+	}
+	log.Printf("PaymentIntent %s failed!", pi.ID)
+	return nil
+}

+ 101 - 0
internal/domain/provisioning/handler.go

@@ -0,0 +1,101 @@
+package provisioning
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"strconv"
+
+	"git.linuxforward.com/byom/byom-onboard/internal/common/models"
+	"git.linuxforward.com/byom/byom-onboard/internal/platform/database"
+	"github.com/gin-gonic/gin"
+)
+
+type Handler struct {
+	client ProviderClient
+	db     *database.Database
+}
+
+func NewHandler(client ProviderClient, db *database.Database) *Handler {
+	return &Handler{
+		client: client,
+	}
+}
+
+func (h *Handler) AddVPS(c *gin.Context) {
+	var req models.AddVPSRequest
+	if err := c.ShouldBindJSON(&req); err != nil {
+		c.JSON(400, gin.H{"error": err.Error()})
+		return
+	}
+
+	vps := models.VPSInstance{
+		DisplayName: req.DisplayName,
+		IpAddress:   req.IpAddress,
+		Plan:        req.Plan,
+	}
+
+	if err := h.db.AddVPS(&vps); err != nil {
+		c.JSON(500, gin.H{"error": err.Error()})
+		return
+	}
+
+	c.JSON(200, gin.H{"message": "VPS added successfully"})
+}
+
+func (h *Handler) ListVPS(c *gin.Context) {
+	instances, err := h.db.ListVPS()
+	if err != nil {
+		c.JSON(500, gin.H{"error": err.Error()})
+		return
+	}
+
+	c.JSON(200, instances)
+}
+
+var mapPlanToVPS = map[string]string{
+	"basic":      "1-2-40",
+	"pro":        "2-4-80",
+	"enterprise": "16-16-160",
+}
+
+func (h *Handler) FindVPSForPlan(plan string, userID uint) (string, string, error) {
+	vps, ok := mapPlanToVPS[plan]
+	if !ok {
+		return "", "", fmt.Errorf("plan not found")
+	}
+
+	availableVPS, err := h.db.GetAvailableVPS(context.Background(), vps)
+	if err != nil {
+		return "", "", err
+	}
+
+	//assign the vps to the user
+	id, err := strconv.ParseUint(availableVPS[0].ID, 10, 32)
+	if err != nil {
+		return "", "", err
+	}
+	if err := h.db.AssignVPSToUser(uint(id), userID); err != nil {
+		return "", "", err
+	}
+
+	log.Println("Found", len(availableVPS), "available VPS for plan", plan)
+
+	return availableVPS[0].DisplayName, availableVPS[0].IpAddress, nil
+}
+
+func (h *Handler) GetVPSStatus(c *gin.Context) {
+	var req models.GetVPSStatusRequest
+	if err := c.ShouldBindJSON(&req); err != nil {
+		c.JSON(400, gin.H{"error": err.Error()})
+		return
+	}
+
+	status, err := h.client.GetVPSStatus(req.ID)
+	if err != nil {
+		c.JSON(500, gin.H{"error": err.Error()})
+		return
+	}
+
+	c.JSON(200, gin.H{"status": status})
+}

+ 40 - 0
internal/domain/provisioning/ovh.go

@@ -0,0 +1,40 @@
+package provisioning
+
+import (
+	"github.com/ovh/go-ovh/ovh"
+)
+
+type Ovh struct {
+	ovhClient *ovh.Client
+}
+
+func NewOvhClient(endpoint, appKey, appSecret string) *Ovh {
+	ovhClient, _ := ovh.NewClient(
+		endpoint,
+		appKey,
+		appSecret,
+		"",
+	)
+	// if err != nil {
+	// 	panic(err)
+	// }
+
+	return &Ovh{
+		ovhClient: ovhClient,
+	}
+}
+
+func (o *Ovh) ConfigureZone(zoneName string) error {
+	err := o.ovhClient.Post("/domain/zone", map[string]interface{}{
+		"zone": zoneName,
+	}, nil)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (o *Ovh) GetVPSStatus(id string) (string, error) {
+	return "", nil
+}

+ 6 - 0
internal/domain/provisioning/service.go

@@ -0,0 +1,6 @@
+package provisioning
+
+type ProviderClient interface {
+	ConfigureZone(zoneName string) error
+	GetVPSStatus(id string) (string, error)
+}

+ 68 - 0
internal/domain/register/email.go

@@ -0,0 +1,68 @@
+package register
+
+import (
+	"time"
+
+	"git.linuxforward.com/byom/byom-onboard/internal/common/jwt"
+	"git.linuxforward.com/byom/byom-onboard/internal/common/models"
+	"git.linuxforward.com/byom/byom-onboard/internal/platform/database"
+	"github.com/google/uuid"
+)
+
+type registrationService struct {
+	db  *database.Database
+	jwt *jwt.JWTClient
+}
+
+func NewRegistrationService(db *database.Database, jwt *jwt.JWTClient) RegistrationService {
+	return &registrationService{
+		db:  db,
+		jwt: jwt,
+	}
+}
+
+func (s *registrationService) InitiateRegistration(user *models.User) error {
+	// Generate email token
+	token, err := s.jwt.GenerateEmailVerificationToken(user.Email)
+	if err != nil {
+		return err
+	}
+
+	user.Token = token
+	user.Status = false
+
+	// Save registration details to database
+	if err := s.db.RegisterUser(user); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *registrationService) SendEmail() error {
+	// Send email
+	return nil
+}
+
+func (s *registrationService) ValidateEmail(token string) (*ValidationResult, error) {
+	email, err := s.jwt.VerifyEmailToken(token)
+	if err != nil {
+		return nil, err
+	}
+
+	// Validate email
+	if err := s.db.ChangeSubscriptionStatus(email); err != nil {
+		return nil, err
+	}
+
+	// For now, return a dummy validation result
+	return &ValidationResult{
+		Email:        email, // This should come from token validation
+		ValidatedAt:  time.Now(),
+		SessionToken: uuid.New().String(),
+	}, nil
+}
+
+func (s *registrationService) CreateSessionToken(email string) (string, error) {
+	return uuid.New().String(), nil
+}

+ 49 - 0
internal/domain/register/handler.go

@@ -0,0 +1,49 @@
+package register
+
+import (
+	"git.linuxforward.com/byom/byom-onboard/internal/common/models"
+	"git.linuxforward.com/byom/byom-onboard/internal/platform/database"
+	"git.linuxforward.com/byom/byom-onboard/internal/platform/mailer"
+	"github.com/gin-gonic/gin"
+)
+
+type Handler struct {
+	registrationService RegistrationService
+	mailer              *mailer.Mailer
+	db                  *database.Database
+}
+
+func NewHandler(client RegistrationService, mailerClient *mailer.Mailer, db *database.Database) *Handler {
+	return &Handler{
+		registrationService: client,
+		mailer:              mailerClient,
+		db:                  db,
+	}
+}
+
+func (h *Handler) Register(c *gin.Context) {
+	var req models.User
+	if err := c.ShouldBindJSON(&req); err != nil {
+		c.JSON(400, gin.H{"error": err.Error()})
+		return
+	}
+
+	if err := h.registrationService.InitiateRegistration(&req); err != nil {
+		c.JSON(500, gin.H{"error": err.Error()})
+		return
+	}
+
+	c.JSON(200, gin.H{"message": "Registration initiated successfully"})
+}
+
+func (h *Handler) ValidateEmail(c *gin.Context) {
+	token := c.Query("token")
+
+	result, err := h.registrationService.ValidateEmail(token)
+	if err != nil {
+		c.JSON(500, gin.H{"error": err.Error()})
+		return
+	}
+
+	c.JSON(200, result)
+}

+ 15 - 0
internal/domain/register/models.go

@@ -0,0 +1,15 @@
+package register
+
+import "time"
+
+type ValidationResult struct {
+	Email        string    `json:"email"`
+	ValidatedAt  time.Time `json:"validated_at"`
+	SessionToken string    `json:"session_token"`
+}
+
+type RegistrationStatus struct {
+	Email     string    `json:"email"`
+	Step      string    `json:"step"` // "email_sent", "email_validated", "plan_selected", "payment_pending"
+	UpdatedAt time.Time `json:"updated_at"`
+}

+ 9 - 0
internal/domain/register/service.go

@@ -0,0 +1,9 @@
+package register
+
+import "git.linuxforward.com/byom/byom-onboard/internal/common/models"
+
+type RegistrationService interface {
+	InitiateRegistration(user *models.User) error
+	ValidateEmail(token string) (*ValidationResult, error)
+	CreateSessionToken(email string) (string, error)
+}

+ 120 - 0
internal/platform/config/config.go

@@ -0,0 +1,120 @@
+package config
+
+import (
+	"crypto/rand"
+	"fmt"
+	"math/big"
+	"time"
+
+	"github.com/spf13/viper"
+)
+
+type Config struct {
+	Server   ServerConfig
+	Database DatabaseConfig
+	Stripe   StripeConfig
+	OVH      OVHConfig
+	Mailer   MailerConfig
+	Logging  LoggingConfig
+	JWT      JWT
+}
+
+type ServerConfig struct {
+	Address      string
+	Port         int
+	ReadTimeout  time.Duration
+	WriteTimeout time.Duration
+	IdleTimeout  time.Duration
+	Tls          TlsConfig
+}
+
+type DatabaseConfig struct {
+	DSN string
+}
+
+type StripeConfig struct {
+	SecretKey     string
+	WebhookSecret string
+}
+
+type OVHConfig struct {
+	Endpoint    string
+	AppKey      string
+	AppSecret   string
+	ConsumerKey string
+}
+
+type MailerConfig struct {
+	Identity string
+	Username string
+	Password string
+	Host     string
+	Port     string
+	From     string
+}
+
+type TlsConfig struct {
+	CertFile string
+	KeyFile  string
+}
+type LoggingConfig struct {
+	Level  string
+	Format string
+}
+
+type JWT struct {
+	Secret string
+}
+
+func Load() (*Config, error) {
+	viper.SetConfigName("config")
+	viper.SetConfigType("yaml")
+	viper.AddConfigPath(".")
+	viper.AddConfigPath("/etc/byom/")
+
+	// Set defaults
+	viper.SetDefault("server.port", 8080)
+	viper.SetDefault("server.readTimeout", "15s")
+	viper.SetDefault("server.writeTimeout", "15s")
+	viper.SetDefault("server.idleTimeout", "60s")
+
+	viper.SetDefault("database.dsn", ".trash/trash.db")
+	//generate random secret for jwt
+	secret, err := GenerateRandomString(256)
+	if err != nil {
+		return nil, fmt.Errorf("error generating random string: %w", err)
+	}
+
+	viper.SetDefault("jwt.secret", secret)
+
+	// Environment variables
+	viper.AutomaticEnv()
+	viper.SetEnvPrefix("BYOM")
+
+	if err := viper.ReadInConfig(); err != nil {
+		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
+			return nil, fmt.Errorf("error reading config: %w", err)
+		}
+	}
+
+	var config Config
+	if err := viper.Unmarshal(&config); err != nil {
+		return nil, fmt.Errorf("error unmarshaling config: %w", err)
+	}
+
+	return &config, nil
+}
+
+func GenerateRandomString(n int) (string, error) {
+	const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+	ret := make([]byte, n)
+	for i := 0; i < n; i++ {
+		num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
+		if err != nil {
+			return "", err
+		}
+		ret[i] = letters[num.Int64()]
+	}
+
+	return string(ret), nil
+}

+ 65 - 0
internal/platform/database/database.go

@@ -0,0 +1,65 @@
+package database
+
+import (
+	"fmt"
+	"time"
+
+	"git.linuxforward.com/byom/byom-onboard/internal/common/models"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+)
+
+type Database struct {
+	db     *gorm.DB
+	config *Config
+}
+
+type Config struct {
+	MaxOpenConns    int
+	MaxIdleConns    int
+	ConnMaxLifetime time.Duration
+	ConnMaxIdleTime time.Duration
+}
+
+var DefaultConfig = &Config{
+	MaxOpenConns:    10,
+	MaxIdleConns:    2,
+	ConnMaxLifetime: 30 * time.Second,
+	ConnMaxIdleTime: 30 * time.Second,
+}
+
+func NewDatabase(dsn string) (*Database, error) {
+	gormConfig := &gorm.Config{
+		Logger: logger.Default.LogMode(logger.Info),
+		NowFunc: func() time.Time {
+			return time.Now().UTC()
+		},
+	}
+
+	db, err := gorm.Open(sqlite.Open(dsn), gormConfig)
+	if err != nil {
+		return nil, fmt.Errorf("failed to connect to database: %w", err)
+	}
+
+	sqlDB, err := db.DB()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get database instance: %w", err)
+	}
+
+	// define
+
+	// Configure connection pool
+	sqlDB.SetMaxOpenConns(DefaultConfig.MaxOpenConns)
+	sqlDB.SetMaxIdleConns(DefaultConfig.MaxIdleConns)
+	sqlDB.SetConnMaxLifetime(DefaultConfig.ConnMaxLifetime)
+	sqlDB.SetConnMaxIdleTime(DefaultConfig.ConnMaxIdleTime)
+
+	// Migrate the schema
+	err = db.AutoMigrate(&models.User{})
+	if err != nil {
+		return nil, fmt.Errorf("failed to migrate schema: %w", err)
+	}
+
+	return &Database{db: db, config: DefaultConfig}, nil
+}

+ 59 - 0
internal/platform/database/provision.go

@@ -0,0 +1,59 @@
+package database
+
+import (
+	"context"
+	"fmt"
+
+	"git.linuxforward.com/byom/byom-onboard/internal/common/errors"
+	"git.linuxforward.com/byom/byom-onboard/internal/common/models"
+)
+
+func (d *Database) AddVPS(vps *models.VPSInstance) error {
+	return d.db.Create(vps).Error
+}
+
+func (d *Database) GetAvailableVPS(ctx context.Context, plan string) ([]models.VPSInstance, error) {
+	var vps []models.VPSInstance
+	result := d.db.WithContext(ctx).
+		Where("plan = ? AND is_in_use = ?", plan, false).
+		Find(&vps)
+
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get available VPS: %w", result.Error)
+	}
+
+	if len(vps) == 0 {
+		return nil, errors.ErrNoVPSAvailable
+	}
+
+	return vps, nil
+}
+
+func (d *Database) ListVPS() ([]models.VPSInstance, error) {
+	var vps []models.VPSInstance
+	rowzs := d.db.Find(&vps)
+
+	if rowzs.Error != nil {
+		return nil, rowzs.Error
+	}
+
+	return vps, nil
+}
+
+func (d *Database) AssignVPSToUser(vpsID uint, userID uint) error {
+	return d.db.Model(&models.VPSInstance{}).
+		Where("id = ?", vpsID).
+		Updates(map[string]interface{}{
+			"is_in_use": true,
+			"user_id":   userID,
+		}).Error
+}
+
+func (d *Database) UnassignVPS(vpsID uint) error {
+	return d.db.Model(&models.VPSInstance{}).
+		Where("id = ?", vpsID).
+		Updates(map[string]interface{}{
+			"is_in_use": false,
+			"user_id":   nil,
+		}).Error
+}

+ 51 - 0
internal/platform/database/register.go

@@ -0,0 +1,51 @@
+package database
+
+import (
+	"git.linuxforward.com/byom/byom-onboard/internal/common/models"
+)
+
+func (d *Database) RegisterUser(user *models.User) error {
+	// Save registration details to database
+	err := d.db.Create(user).Error
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (d *Database) ChangeSubscriptionStatus(email string) error {
+	// Change the status of the user to active
+	user, err := d.getUserByEmail(email)
+	if err != nil {
+		return err
+	}
+
+	user.Status = true
+
+	err = d.updateUser(user)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (d *Database) getUserByEmail(email string) (*models.User, error) {
+	var user *models.User
+	err := d.db.Where("email = ?", email).First(&user).Error
+	if err != nil {
+		return nil, err
+	}
+
+	return user, nil
+}
+
+func (d *Database) updateUser(user *models.User) error {
+	err := d.db.Save(user).Error
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 85 - 0
internal/platform/mailer/mailer.go

@@ -0,0 +1,85 @@
+package mailer
+
+import (
+	"bytes"
+	"html/template"
+	"net/smtp"
+
+	"git.linuxforward.com/byom/byom-onboard/internal/platform/config"
+)
+
+const welcomeEmailTemplate = `
+Bienvenue sur Byom!
+
+Votre application a été configurée avec succès et est prête à être utilisée.
+
+Informations d'accès :
+---------------------
+Nom d'utilisateur : {{.Username}}
+Mot de passe : {{.Password}}
+
+URL de votre application :
+--------------------------
+{{range .Subdomains}}
+
+{{.}}
+{{end}}
+Application principale : {{.WebAppURL}}
+
+Pour bien commencer :
+---------------------
+{{.SetupGuide}}
+
+Pour des raisons de sécurité, nous vous recommandons de changer votre mot de passe lors de votre première connexion.
+
+Si vous avez besoin d'aide, n'hésitez pas à contacter notre équipe d'assistance.
+
+Cordialement,
+Byom
+`
+
+type Mailer struct {
+	auth       *smtp.Auth
+	serverAddr string
+	from       string
+}
+
+type EmailData struct {
+	Username   string
+	Password   string
+	Subdomains []string
+	WebAppURL  string
+	SetupGuide string
+}
+
+func NewMailer(config *config.MailerConfig) *Mailer {
+
+	auth := smtp.PlainAuth(
+		config.Identity,
+		config.Username,
+		config.Password,
+		config.Host,
+	)
+
+	serverAddr := config.Host + ":" + config.Port
+
+	return &Mailer{
+		auth:       &auth,
+		serverAddr: serverAddr,
+		from:       config.Username,
+	}
+}
+
+func (m *Mailer) SendEmail(to string, data *EmailData) error {
+	tmpl := template.Must(template.New("welcomeEmail").Parse(welcomeEmailTemplate))
+	var tpl bytes.Buffer
+
+	err := tmpl.Execute(&tpl, data)
+	if err != nil {
+		return err
+	}
+
+	body := tpl.String()
+
+	return smtp.SendMail(m.serverAddr, *m.auth, m.from, []string{to}, []byte(body))
+}

+ 107 - 0
internal/platform/mailer/mailer_test.go

@@ -0,0 +1,107 @@
+// mailer_test.go
+package mailer
+
+import (
+	"bytes"
+	"fmt"
+	"html/template"
+	"strings"
+	"testing"
+
+	"git.linuxforward.com/byom/byom-onboard/internal/platform/config"
+)
+
+func TestNewMailer(t *testing.T) {
+	cfg := &config.MailerConfig{
+		Identity: "",
+		Username: "test@example.com",
+		Password: "testpass",
+		Host:     "localhost",
+		Port:     "2525",
+	}
+
+	mailer := NewMailer(cfg)
+
+	if mailer == nil {
+		t.Error("Expected non-nil mailer")
+	}
+
+	if mailer.serverAddr != "localhost:2525" {
+		t.Errorf("Expected server address 'localhost:2525', got %s", mailer.serverAddr)
+	}
+
+	if mailer.from != "test@example.com" {
+		t.Errorf("Expected from address 'test@example.com', got %s", mailer.from)
+	}
+}
+
+func TestSendEmail(t *testing.T) {
+	// Create test data
+	data := &EmailData{
+		Username:   "testuser",
+		Password:   "testpass",
+		Subdomains: []string{"app1.example.com", "app2.example.com"},
+		WebAppURL:  "https://main.example.com",
+		SetupGuide: "1. Log in\n2. Configure settings",
+	}
+
+	// Create mailer with mock configuration
+	cfg := &config.MailerConfig{
+		Identity: "",
+		Username: "test@example.com",
+		Password: "testpass",
+		Host:     "localhost",
+		Port:     "2525",
+	}
+
+	mailer := NewMailer(cfg)
+
+	// Test email sending
+	err := mailer.SendEmail("recipient@example.com", data)
+	if err != nil {
+		t.Errorf("Expected no error, got %v", err)
+	}
+
+	// Test email content
+	// Note: In a real implementation, you might want to capture the email content
+	// using a mock SMTP server and verify its contents
+}
+
+// TestEmailTemplateRendering tests the template rendering separately
+func TestEmailTemplateRendering(t *testing.T) {
+	data := &EmailData{
+		Username:   "testuser",
+		Password:   "testpass",
+		Subdomains: []string{"app1.example.com", "app2.example.com"},
+		WebAppURL:  "https://main.example.com",
+		SetupGuide: "1. Log in\n2. Configure settings",
+	}
+
+	tmpl := template.Must(template.New("welcomeEmail").Parse(welcomeEmailTemplate))
+	var tpl bytes.Buffer
+
+	err := tmpl.Execute(&tpl, data)
+	if err != nil {
+		t.Errorf("Template execution failed: %v", err)
+	}
+
+	result := tpl.String()
+
+	fmt.Println(result)
+
+	// Verify that all required information is present in the rendered template
+	expectedContents := []string{
+		data.Username,
+		data.Password,
+		data.Subdomains[0],
+		data.Subdomains[1],
+		data.WebAppURL,
+		data.SetupGuide,
+	}
+
+	for _, expected := range expectedContents {
+		if !strings.Contains(result, expected) {
+			t.Errorf("Expected email to contain '%s'", expected)
+		}
+	}
+}

+ 22 - 0
internal/platform/middleware/admin.go

@@ -0,0 +1,22 @@
+package middleware
+
+import "github.com/gin-gonic/gin"
+
+type AdminMiddleware struct {
+	accounts *gin.Accounts
+}
+
+func NewAdminMiddleware() *AdminMiddleware {
+
+	var accounts = gin.Accounts{
+		"admin": "admin",
+	}
+
+	return &AdminMiddleware{
+		accounts: &accounts,
+	}
+}
+
+func (a *AdminMiddleware) Middleware() gin.HandlerFunc {
+	return gin.BasicAuth(*a.accounts)
+}

+ 4 - 0
internal/platform/middleware/basic.go

@@ -0,0 +1,4 @@
+package middleware
+
+type Basic struct {
+}