h0kd0k 1 hónapja
szülő
commit
60b3ad55ad

+ 35 - 0
.gitignore

@@ -0,0 +1,35 @@
+# Binaries
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with 'go test -c'
+*.test
+
+# Output of the go coverage tool
+*.out
+
+# Dependency directories
+vendor/
+
+# IDE specific files
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# Environment variables
+.env
+.env.local
+
+# Build output
+bin/
+dist/
+
+# Local test
+.trash/
+config.yaml
+.data/
+data/

+ 28 - 2
cmd/api/main.go

@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/alecthomas/kong"
+	"github.com/gin-contrib/cors"
 	"github.com/gin-gonic/gin"
 
 	"git.linuxforward.com/byom/byom-onboard/internal/common/jwt"
@@ -19,6 +20,7 @@ import (
 	"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/payment"
 )
 
 var (
@@ -63,6 +65,13 @@ func (s *ServeCmd) Run(cli *CLI) error {
 	r.Use(gin.Logger())
 	r.Use(gin.Recovery())
 
+	corsConfig := cors.DefaultConfig()
+	corsConfig.AllowOrigins = []string{"http://192.168.1.35:5173"}
+	corsConfig.AllowCredentials = true
+	corsConfig.AllowHeaders = []string{"Content-Type", "Authorization"}
+	corsConfig.AllowMethods = []string{"*"}
+	r.Use(cors.New(corsConfig))
+
 	if cli.Debug {
 		log.Printf("Debug mode enabled")
 		gin.SetMode(gin.DebugMode)
@@ -78,7 +87,7 @@ func (s *ServeCmd) Run(cli *CLI) error {
 	jwtClient := jwt.NewJWTClient([]byte(cfg.JWT.Secret))
 
 	// Initialize services
-	stripeClient := billing.NewStripeClient(cfg.Stripe.SecretKey, cfg.Stripe.WebhookSecret)
+	stripeClient := billing.NewStripeClient(cfg.Stripe.APIKey, cfg.Stripe.EndpointSecret)
 	mailerClient := mailer.NewMailer(&cfg.Mailer)
 	//ovhClient := provisioning.NewOvhClient(cfg.OVH.Endpoint, cfg.OVH.AppKey, cfg.OVH.AppSecret)
 
@@ -86,8 +95,20 @@ func (s *ServeCmd) Run(cli *CLI) error {
 	billingHandler := billing.NewHandler(stripeClient, db)
 	//provisioningHandler := provisioning.NewHandler(ovhClient, db)
 	registerHandler := register.NewHandler(mailerClient, db, jwtClient)
+
+	// Initialize Stripe handler
+	stripeHandler, err := payment.NewStripeHandler(
+		cfg.Stripe.APIKey,
+		"whsec_527fbb8ce7f9072a60a17a37e2807965c08a10fb48aa218e9ed14b7835520844",
+		"http://192.168.1.35:5173",
+		mailerClient,
+	)
+	if err != nil {
+		return fmt.Errorf("failed to initialize Stripe: %w", err)
+	}
+
 	// Setup server
-	server := setupServer(r, billingHandler, registerHandler, cfg)
+	server := setupServer(r, billingHandler, registerHandler, cfg, stripeHandler)
 	server.Addr = fmt.Sprintf(":%d", s.Port)
 
 	// Start server in a goroutine
@@ -120,6 +141,7 @@ func setupServer(
 	billingHandler *billing.Handler,
 	registerHandler *register.Handler,
 	config *config.Config,
+	stripeHandler *payment.StripeHandler,
 ) *http.Server {
 
 	api := r.Group("/api/v1")
@@ -133,6 +155,10 @@ func setupServer(
 		api.POST("/payment", billingHandler.CreatePayment)
 		api.POST("/payment/webhook", billingHandler.HandleWebhook)
 
+		// Stripe routes
+		api.POST("/create-checkout-session", stripeHandler.CreateCheckoutSession)
+		api.POST("/create-portal-session", stripeHandler.CreatePortalSession)
+		api.POST("/webhook", stripeHandler.HandleWebhook)
 	}
 
 	return &http.Server{

+ 78 - 0
config.sample.yml

@@ -0,0 +1,78 @@
+# 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: "ssl0.ovh.net"
+  username: "mail"
+  password: "pass"
+  from: "BYOM Service <no-reply@byom.fr>"
+
+# 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

+ 11 - 10
go.mod

@@ -9,24 +9,24 @@ require (
 
 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/bytedance/sonic v1.12.6 // indirect
+	github.com/bytedance/sonic/loader v0.2.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/gabriel-vasile/mimetype v1.4.7 // 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/go-playground/validator/v10 v10.23.0 // indirect
+	github.com/goccy/go-json v0.10.4 // 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/klauspost/cpuid/v2 v2.2.9 // 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
@@ -34,7 +34,7 @@ require (
 	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/pelletier/go-toml/v2 v2.2.3 // 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
@@ -47,24 +47,25 @@ require (
 	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/arch v0.12.0 // indirect
 	golang.org/x/crypto v0.32.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.29.0 // indirect
 	golang.org/x/text v0.21.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
-	google.golang.org/protobuf v1.34.1 // indirect
+	google.golang.org/protobuf v1.36.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
 require (
+	github.com/gin-contrib/cors v1.7.3
 	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
 	github.com/wneessen/go-mail v0.6.1
-	golang.org/x/net v0.25.0 // indirect
+	golang.org/x/net v0.33.0 // indirect
 	gorm.io/gorm v1.25.12
 )

+ 22 - 0
go.sum

@@ -2,8 +2,12 @@ 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 v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
+github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
 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/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
+github.com/bytedance/sonic/loader v0.2.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=
@@ -16,6 +20,10 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
 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/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
+github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
+github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns=
+github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4=
 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=
@@ -28,8 +36,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
 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/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
+github.com/go-playground/validator/v10 v10.23.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/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
+github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 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=
@@ -56,6 +68,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
 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/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
+github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
 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=
@@ -76,6 +90,8 @@ 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/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
+github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
 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=
@@ -123,6 +139,8 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
 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/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
+golang.org/x/arch v0.12.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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
@@ -149,6 +167,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 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/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 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=
@@ -213,6 +233,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
 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=
+google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
+google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 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=

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

@@ -31,6 +31,7 @@ type User struct {
 	Email   string `json:"email"`
 	Token   string `json:"token"`
 	Status  bool   `json:"status"`
+	Plan    string `json:"plan"`
 }
 
 type VPSInstance struct {

+ 2 - 2
internal/domain/register/handler.go

@@ -47,7 +47,7 @@ func (h *Handler) Register(c *gin.Context) {
 	}
 
 	// Send email
-	if err := h.mailer.SendVerifyEmail(req.Email, token); err != nil {
+	if err := h.mailer.SendVerifyEmail(req.Email, token, req.Plan); err != nil {
 		c.JSON(500, gin.H{"error": err.Error()})
 		return
 	}
@@ -81,7 +81,7 @@ func (h *Handler) CheckEmail(c *gin.Context) {
 	}
 
 	// Send email
-	if err := h.mailer.SendVerifyEmail(req.Email, token); err != nil {
+	if err := h.mailer.SendVerifyEmail(req.Email, token, req.Plan); err != nil {
 		c.JSON(500, gin.H{"error": err.Error()})
 		return
 	}

+ 10 - 2
internal/platform/config/config.go

@@ -33,8 +33,8 @@ type DatabaseConfig struct {
 }
 
 type StripeConfig struct {
-	SecretKey     string
-	WebhookSecret string
+	APIKey         string `mapstructure:"api_key" env:"STRIPE_API_KEY"`
+	EndpointSecret string `mapstructure:"endpoint_secret" env:"STRIPE_WEBHOOK_SECRET"`
 }
 
 type OVHConfig struct {
@@ -91,6 +91,14 @@ func Load() (*Config, error) {
 	viper.AutomaticEnv()
 	viper.SetEnvPrefix("BYOM")
 
+	// Bind environment variables
+	viper.BindEnv("stripe.api_key", "STRIPE_API_KEY")
+	viper.BindEnv("stripe.endpoint_secret", "STRIPE_WEBHOOK_SECRET")
+
+	// Set defaults if needed
+	viper.SetDefault("stripe.api_key", "")
+	viper.SetDefault("stripe.endpoint_secret", "")
+
 	if err := viper.ReadInConfig(); err != nil {
 		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
 			return nil, fmt.Errorf("error reading config: %w", err)

+ 365 - 2
internal/platform/mailer/mailer.go

@@ -5,6 +5,8 @@ import (
 	"fmt"
 	"html/template"
 	"log"
+	"net/url"
+	"strings"
 
 	"github.com/wneessen/go-mail"
 
@@ -84,6 +86,291 @@ const welcomeEmailTemplate = `
 </html>
 `
 
+const verifyEmailTemplate = `
+<!DOCTYPE html>
+<html lang="fr">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>Vérification de votre email - Byom</title>
+	<style>
+		body {
+			font-family: Arial, sans-serif;
+			line-height: 1.6;
+			color: #333;
+			background-color: #f5f5f5;
+			margin: 0;
+			padding: 20px;
+		}
+		.container {
+			max-width: 600px;
+			margin: 0 auto;
+			background-color: #ffffff;
+			border-radius: 12px;
+			box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+			overflow: hidden;
+		}
+		.header {
+			background-color: #0056b3;
+			color: white;
+			padding: 30px;
+			text-align: center;
+		}
+		.header h1 {
+			margin: 0;
+			font-size: 28px;
+			color: white;
+		}
+		.content {
+			padding: 40px 30px;
+		}
+		.message {
+			text-align: center;
+			margin-bottom: 30px;
+			font-size: 16px;
+			color: #555;
+		}
+		.button-container {
+			text-align: center;
+			margin: 30px 0;
+		}
+		.button {
+			display: inline-block;
+			padding: 15px 35px;
+			background-color: #0056b3;
+			color: white;
+			text-decoration: none;
+			border-radius: 50px;
+			font-weight: bold;
+			font-size: 16px;
+			transition: all 0.3s ease;
+			box-shadow: 0 2px 4px rgba(0, 86, 179, 0.3);
+		}
+		.button:hover {
+			background-color: #003d80;
+			transform: translateY(-2px);
+			box-shadow: 0 4px 8px rgba(0, 86, 179, 0.4);
+		}
+		.verification-link {
+			text-align: center;
+			margin: 20px 0;
+			padding: 15px;
+			background-color: #f8f9fa;
+			border-radius: 8px;
+			font-size: 14px;
+			color: #666;
+			word-break: break-all;
+		}
+		.divider {
+			height: 1px;
+			background-color: #eee;
+			margin: 30px 0;
+		}
+		.footer {
+			text-align: center;
+			padding: 20px 30px;
+			background-color: #f8f9fa;
+			color: #666;
+			font-size: 14px;
+		}
+		.logo {
+			margin-bottom: 20px;
+		}
+		.security-notice {
+			background-color: #fff3cd;
+			border-left: 4px solid #ffc107;
+			padding: 15px;
+			margin: 20px 0;
+			font-size: 14px;
+			color: #856404;
+		}
+		.signature {
+			margin-top: 20px;
+			color: #0056b3;
+			font-weight: bold;
+		}
+	</style>
+</head>
+<body>
+	<div class="container">
+		<div class="header">
+			<div class="logo">
+				<!-- Vous pouvez ajouter votre logo ici -->
+				<h1>BYOM</h1>
+			</div>
+			<h2>Vérification de votre email</h2>
+		</div>
+		
+		<div class="content">
+			<div class="message">
+				<h2>Merci de vous être inscrit!</h2>
+				<p>Pour finaliser votre inscription et accéder à toutes les fonctionnalités de Byom, veuillez vérifier votre adresse email.</p>
+			</div>
+
+			<div class="button-container">
+				<a href="{{.VerificationURL}}" class="button">Vérifier mon email</a>
+			</div>
+
+			<div class="security-notice">
+				<strong>🔒 Note de sécurité:</strong>
+				<p>Ce lien expirera dans 24 heures pour des raisons de sécurité.</p>
+			</div>
+
+			<div class="verification-link">
+				<p>Si le bouton ne fonctionne pas, copiez et collez ce lien dans votre navigateur:</p>
+				<p>{{.VerificationURL}}</p>
+			</div>
+
+			<div class="divider"></div>
+
+			<div class="footer">
+				<p>Si vous n'avez pas créé de compte sur Byom, vous pouvez ignorer cet email en toute sécurité.</p>
+				<div class="signature">
+					<p>Cordialement,<br>L'équipe Byom</p>
+				</div>
+			</div>
+		</div>
+	</div>
+</body>
+</html>
+`
+
+const workspaceWelcomeTemplate = `
+<!DOCTYPE html>
+<html lang="fr">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>Votre espace de travail Byom est prêt !</title>
+	<style>
+		body {
+			font-family: Arial, sans-serif;
+			line-height: 1.6;
+			color: #333;
+			background-color: #f5f5f5;
+			margin: 0;
+			padding: 20px;
+		}
+		.container {
+			max-width: 600px;
+			margin: 0 auto;
+			background-color: #ffffff;
+			border-radius: 12px;
+			box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+			overflow: hidden;
+		}
+		.header {
+			background-color: #0056b3;
+			color: white;
+			padding: 30px;
+			text-align: center;
+		}
+		.header h1 {
+			margin: 0;
+			font-size: 28px;
+			color: white;
+		}
+		.content {
+			padding: 40px 30px;
+		}
+		.welcome-message {
+			text-align: center;
+			margin-bottom: 30px;
+			font-size: 18px;
+			color: #333;
+		}
+		.button-container {
+			text-align: center;
+			margin: 30px 0;
+		}
+		.button {
+			display: inline-block;
+			padding: 15px 35px;
+			background-color: #0056b3;
+			color: white;
+			text-decoration: none;
+			border-radius: 50px;
+			font-weight: bold;
+			font-size: 16px;
+			transition: all 0.3s ease;
+			box-shadow: 0 2px 4px rgba(0, 86, 179, 0.3);
+		}
+		.button:hover {
+			background-color: #003d80;
+			transform: translateY(-2px);
+			box-shadow: 0 4px 8px rgba(0, 86, 179, 0.4);
+		}
+		.features {
+			margin: 30px 0;
+			padding: 20px;
+			background-color: #f8f9fa;
+			border-radius: 8px;
+		}
+		.features h3 {
+			color: #0056b3;
+			margin-bottom: 15px;
+		}
+		.features ul {
+			list-style-type: none;
+			padding: 0;
+		}
+		.features li {
+			margin: 10px 0;
+			padding-left: 25px;
+			position: relative;
+		}
+		.features li:before {
+			content: "✓";
+			color: #0056b3;
+			position: absolute;
+			left: 0;
+		}
+		.footer {
+			text-align: center;
+			padding: 20px 30px;
+			background-color: #f8f9fa;
+			color: #666;
+			font-size: 14px;
+		}
+	</style>
+</head>
+<body>
+	<div class="container">
+		<div class="header">
+			<h1>BYOM</h1>
+			<h2>Votre espace de travail est prêt !</h2>
+		</div>
+		
+		<div class="content">
+			<div class="welcome-message">
+				<h2>Félicitations !</h2>
+				<p>Votre paiement a été confirmé et votre espace de travail Byom est maintenant prêt à être utilisé.</p>
+			</div>
+
+			<div class="button-container">
+				<a href="{{.WebAppURL}}" class="button">Accéder à mon espace</a>
+			</div>
+
+			<div class="features">
+				<h3>Ce qui vous attend :</h3>
+				<ul>
+					<li>Interface intuitive et personnalisable</li>
+					<li>Outils de collaboration avancés</li>
+					<li>Support technique dédié</li>
+					<li>Sécurité renforcée</li>
+				</ul>
+			</div>
+
+			<div class="footer">
+				<p>Si vous avez des questions, notre équipe de support est là pour vous aider.</p>
+				<p>Cordialement,<br>L'équipe Byom</p>
+			</div>
+		</div>
+	</div>
+</body>
+</html>
+`
+
 type Mailer struct {
 	client *mail.Client
 	from   string
@@ -97,6 +384,10 @@ type EmailData struct {
 	SetupGuide string
 }
 
+type VerifyEmailData struct {
+	VerificationURL string
+}
+
 func NewMailer(config *config.MailerConfig) *Mailer {
 
 	log.Printf("Test mail successfully delivered.")
@@ -138,15 +429,87 @@ func (m *Mailer) SendEmail(to string, data *EmailData) error {
 	return m.client.DialAndSend(message)
 }
 
-func (m *Mailer) SendVerifyEmail(to, token string) error {
+func (m *Mailer) SendVerifyEmail(to, token, plan string) error {
+	if strings.HasSuffix(to, "@example.com") {
+		return fmt.Errorf("cannot send email to example.com domain: %s", to)
+	}
 
 	message := mail.NewMsg()
 	message.From(m.from)
 	message.To(to)
 	message.Subject("Vérification de votre adresse email")
-	message.SetBodyString(mail.TypeTextHTML, "Cliquez sur le lien suivant pour vérifier votre adresse email: http://localhost:8080/verify-email?token="+token)
+
+	verificationURL := fmt.Sprintf("http://192.168.1.35:5173/verify-email?token=%s&plan=%s&email=%s",
+		token,
+		plan,
+		url.QueryEscape(to),
+	)
+
+	htmlBody := fmt.Sprintf(`
+		<!DOCTYPE html>
+		<html>
+		<head>
+			<meta charset="UTF-8">
+			<title>Vérification de votre email</title>
+		</head>
+		<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
+			<div style="background-color: #ffffff; border-radius: 5px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
+				<h1 style="color: #2c3e50; margin-bottom: 20px;">Bienvenue !</h1>
+				
+				<p style="margin-bottom: 20px;">
+					Merci de vous être inscrit. Pour finaliser votre inscription, veuillez vérifier votre adresse email en cliquant sur le bouton ci-dessous.
+				</p>
+
+				<div style="text-align: center; margin: 30px 0;">
+					<a href="%s" 
+					   style="background-color: #3498db; 
+							  color: white; 
+							  padding: 12px 30px; 
+							  text-decoration: none; 
+							  border-radius: 5px; 
+							  display: inline-block;
+							  font-weight: bold;">
+						Vérifier mon email
+					</a>
+				</div>
+
+				<p style="color: #666; font-size: 14px;">
+					Si le bouton ne fonctionne pas, vous pouvez copier et coller le lien suivant dans votre navigateur :
+					<br>
+					<span style="color: #3498db;">%s</span>
+				</p>
+
+				<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
+
+				<p style="color: #666; font-size: 12px; text-align: center;">
+					Si vous n'avez pas créé de compte, vous pouvez ignorer cet email.
+				</p>
+			</div>
+		</body>
+		</html>
+	`, verificationURL, verificationURL)
+
+	message.SetBodyString(mail.TypeTextHTML, htmlBody)
 	message.ReplyTo(m.from)
 
 	return m.client.DialAndSend(message)
+}
+
+func (m *Mailer) SendWelcomeEmail(to string, data *EmailData) error {
+	tmpl := template.Must(template.New("workspaceWelcome").Parse(workspaceWelcomeTemplate))
+	var tpl bytes.Buffer
 
+	err := tmpl.Execute(&tpl, data)
+	if err != nil {
+		return err
+	}
+
+	message := mail.NewMsg()
+	message.From(m.from)
+	message.To(to)
+	message.Subject("Bienvenue sur votre espace de travail Byom !")
+	message.SetBodyString(mail.TypeTextHTML, tpl.String())
+	message.ReplyTo(m.from)
+
+	return m.client.DialAndSend(message)
 }

+ 193 - 0
internal/platform/payment/stripe.go

@@ -0,0 +1,193 @@
+package payment
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"os"
+
+	"git.linuxforward.com/byom/byom-onboard/internal/platform/mailer"
+	"github.com/gin-gonic/gin"
+	"github.com/stripe/stripe-go/v81"
+	"github.com/stripe/stripe-go/v81/account"
+	portalsession "github.com/stripe/stripe-go/v81/billingportal/session"
+	"github.com/stripe/stripe-go/v81/checkout/session"
+	"github.com/stripe/stripe-go/v81/webhook"
+)
+
+type StripeHandler struct {
+	domain         string
+	endpointSecret string
+	mailer         *mailer.Mailer
+}
+
+func NewStripeHandler(apiKey, endpointSecret, domain string, mailer *mailer.Mailer) (*StripeHandler, error) {
+	stripe.Key = apiKey
+
+	// Test connection by retrieving account information
+	_, err := account.Get()
+	if err != nil {
+		return nil, fmt.Errorf("failed to connect to Stripe: %w", err)
+	}
+
+	return &StripeHandler{
+		domain:         domain,
+		endpointSecret: endpointSecret,
+		mailer:         mailer,
+	}, nil
+}
+
+func (h *StripeHandler) CreateCheckoutSession(c *gin.Context) {
+	// Get email from request
+	customerEmail := c.PostForm("email")
+	if customerEmail == "" {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "Email is required"})
+		return
+	}
+
+	checkoutParams := &stripe.CheckoutSessionParams{
+		Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
+		LineItems: []*stripe.CheckoutSessionLineItemParams{
+			{
+				PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{
+					Currency: stripe.String("eur"),
+					ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{
+						Name: stripe.String("Byom Subscription"),
+					},
+					UnitAmount: stripe.Int64(2000), // 20€ en centimes
+				},
+				Quantity: stripe.Int64(1),
+			},
+		},
+		SuccessURL:       stripe.String("http://192.168.1.35:5173/payment/success?session_id={CHECKOUT_SESSION_ID}"),
+		CancelURL:        stripe.String("http://192.168.1.35:5173/payment/cancel"),
+		CustomerEmail:    stripe.String(customerEmail),
+		CustomerCreation: stripe.String(string(stripe.CheckoutSessionCustomerCreationAlways)),
+	}
+
+	s, err := session.New(checkoutParams)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+		return
+	}
+
+	// Renvoyer uniquement l'URL et l'ID de session
+	c.JSON(http.StatusOK, gin.H{
+		"url":       s.URL,
+		"sessionId": s.ID,
+	})
+}
+
+func (h *StripeHandler) CreatePortalSession(c *gin.Context) {
+	sessionID := c.PostForm("session_id")
+
+	s, err := session.Get(sessionID, nil)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+		log.Printf("session.Get: %v", err)
+		return
+	}
+
+	params := &stripe.BillingPortalSessionParams{
+		Customer:  stripe.String(s.Customer.ID),
+		ReturnURL: stripe.String(h.domain),
+	}
+	ps, err := portalsession.New(params)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+		log.Printf("ps.New: %v", err)
+		return
+	}
+
+	c.Redirect(http.StatusSeeOther, ps.URL)
+}
+
+func (h *StripeHandler) HandleWebhook(c *gin.Context) {
+	const MaxBodyBytes = int64(65536)
+	bodyReader := http.MaxBytesReader(c.Writer, c.Request.Body, MaxBodyBytes)
+	payload, err := io.ReadAll(bodyReader)
+	if err != nil {
+		log.Printf("[Webhook Debug] Error reading body: %v", err)
+		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service unavailable"})
+		return
+	}
+
+	log.Printf("[Webhook Debug] Received payload: %s", string(payload))
+
+	signatureHeader := c.Request.Header.Get("Stripe-Signature")
+	log.Printf("[Webhook Debug] Stripe-Signature header: %s", signatureHeader)
+	log.Printf("[Webhook Debug] Using endpoint secret: %s", h.endpointSecret)
+
+	event, err := webhook.ConstructEvent(payload, signatureHeader, h.endpointSecret)
+	if err != nil {
+		log.Printf("[Webhook Debug] Signature verification failed: %v", err)
+		log.Printf("[Webhook Debug] Headers received: %+v", c.Request.Header)
+		c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request"})
+		return
+	}
+
+	log.Printf("[Webhook Debug] Event type received: %s", event.Type)
+	log.Printf("[Webhook Debug] Event data: %s", string(event.Data.Raw))
+
+	switch event.Type {
+	case "customer.subscription.deleted":
+		var subscription stripe.Subscription
+		if err := json.Unmarshal(event.Data.Raw, &subscription); err != nil {
+			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+			return
+		}
+		log.Printf("Subscription deleted for %s.", subscription.ID)
+
+	case "customer.subscription.updated":
+		var subscription stripe.Subscription
+		if err := json.Unmarshal(event.Data.Raw, &subscription); err != nil {
+			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+			return
+		}
+		log.Printf("Subscription updated for %s.", subscription.ID)
+
+	case "customer.subscription.created":
+		var subscription stripe.Subscription
+		if err := json.Unmarshal(event.Data.Raw, &subscription); err != nil {
+			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+			return
+		}
+		log.Printf("Subscription created for %s.", subscription.ID)
+
+	case "checkout.session.completed":
+		var session stripe.CheckoutSession
+		if err := json.Unmarshal(event.Data.Raw, &session); err != nil {
+			log.Printf("[Webhook Debug] Failed to parse session data: %v", err)
+			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+			return
+		}
+		log.Printf("[Webhook Debug] Payment successful for session: %s", session.ID)
+
+		customerEmail := session.CustomerEmail
+		log.Printf("[Webhook Debug] Customer email: %s", customerEmail)
+
+		if err := h.mailer.SendWelcomeEmail(customerEmail, &mailer.EmailData{
+			Username:   customerEmail,
+			WebAppURL:  fmt.Sprintf("http://test.byom.fr/login?email=%s", customerEmail),
+			SetupGuide: "Votre espace de travail est prêt ! Connectez-vous pour commencer à utiliser Byom.",
+		}); err != nil {
+			log.Printf("[Webhook Debug] Failed to send welcome email: %v", err)
+		}
+
+	case "checkout.session.expired":
+		var session stripe.CheckoutSession
+		if err := json.Unmarshal(event.Data.Raw, &session); err != nil {
+			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+			return
+		}
+		log.Printf("Payment session expired: %s", session.ID)
+		// TODO: Gérer l'expiration de la session
+
+	default:
+		fmt.Fprintf(os.Stderr, "Unhandled event type: %s\n", event.Type)
+	}
+
+	c.JSON(http.StatusOK, gin.H{"status": "success"})
+}