Bläddra i källkod

init byom-gateway

lblt 1 månad sedan
incheckning
332f0396fc
22 ändrade filer med 1541 tillägg och 0 borttagningar
  1. 8 0
      .dockerignore
  2. 35 0
      .gitignore
  3. 29 0
      Dockerfile
  4. 137 0
      README.md
  5. 228 0
      app/app.go
  6. 21 0
      config.sample.yaml
  7. 135 0
      config/config.go
  8. 143 0
      errors/errors.go
  9. 45 0
      go.mod
  10. 126 0
      go.sum
  11. 106 0
      handlers/users.go
  12. 64 0
      logger/logger.go
  13. 41 0
      main.go
  14. 50 0
      middleware/cors.go
  15. 37 0
      middleware/error_handler.go
  16. 50 0
      middleware/recovery.go
  17. 31 0
      middleware/request_id.go
  18. 55 0
      middleware/request_logger.go
  19. 98 0
      middleware/security.go
  20. 61 0
      store/interfaces.go
  21. 41 0
      store/users.go
  22. 0 0
      test.json

+ 8 - 0
.dockerignore

@@ -0,0 +1,8 @@
+.git
+.gitignore
+.env
+*.md
+data/
+docs/
+docker-compose.yml
+Dockerfile 

+ 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/

+ 29 - 0
Dockerfile

@@ -0,0 +1,29 @@
+# Build stage
+FROM golang:1.22.10-alpine AS builder
+
+RUN apk add --no-cache git make gcc musl-dev sqlite-dev
+
+WORKDIR /app
+
+COPY go.mod go.sum ./
+RUN go mod download
+COPY . .
+RUN CGO_ENABLED=1 GOOS=linux go build -o byom-gateway .
+
+
+FROM alpine:3.19
+
+RUN apk add --no-cache ca-certificates tzdata sqlite sqlite-dev
+RUN adduser -D -H -h /app appuser
+WORKDIR /app
+COPY --from=builder /app/byom-gateway .
+COPY config.yaml .
+
+RUN mkdir -p /app/data && \
+    chown -R appuser:appuser /app
+
+USER appuser
+EXPOSE 8443
+ENV CONFIG_FILE=/app/config.yaml
+
+ENTRYPOINT ["./byom-gateway", "-config=/app/config.yaml"]

+ 137 - 0
README.md

@@ -0,0 +1,137 @@
+I'll help you add documentation by creating a README.md file and adding some package-level documentation. Let's start with the main README:
+
+```markdown:README.md
+# BYOM Gateway
+
+BYOM Gateway is a Go-based service that manages user-host associations with a secure REST API.
+
+## Features
+
+- RESTful API for user-host management
+- SQLite database with WAL mode for data persistence
+- Secure by default with comprehensive security middleware
+- Structured logging and request tracing
+- Configuration via YAML
+- Basic authentication support
+
+## Prerequisites
+
+- Go 1.19 or higher
+- SQLite 3
+
+## Quick Start
+
+1. Clone the repository:
+```bash
+git clone git.linuxforward.com/byom/byom-gateway
+cd byom-gateway
+```
+
+2. Copy the sample config:
+```bash
+cp config.sample.yaml config.yaml
+```
+
+3. Build and run:
+```bash
+go build
+./byom-gateway
+```
+
+## Configuration
+
+Configuration is managed through a YAML file. Example configuration:
+
+```yaml
+server:
+  port: "8080"
+  tls:
+    enabled: false
+    cert: ""
+    key: ""
+
+database:
+  file: "./data/dev.db"
+  log_mode: true
+  max_open_conns: 10
+  max_idle_conns: 5
+  conn_max_lifetime: 3600
+  pragma:
+    - "journal_mode=WAL"
+    - "busy_timeout=5000"
+    - "foreign_keys=ON"
+
+basic_auth:
+  username: "admin"
+  password: "your_secure_password"
+```
+
+## API Documentation
+
+### User-Host Management
+
+#### Get User Host
+```http
+GET /api/v1/user-host?email=user@example.com
+```
+
+Response:
+```json
+{
+    "email": "user@example.com",
+    "host": "host.example.com"
+}
+```
+
+#### Add User Host
+```http
+POST /api/v1/user-host
+```
+
+Request body:
+```json
+{
+    "email": "user@example.com",
+    "host": "host.example.com"
+}
+```
+
+## Security
+
+The service implements several security measures:
+
+- CORS protection
+- Security headers (HSTS, CSP, etc.)
+- Request sanitization
+- Basic authentication
+- TLS support
+- Request rate limiting
+
+## Development
+
+### Project Structure
+
+```
+.
+├── app/            # Application initialization and setup
+├── config/         # Configuration handling
+├── errors/         # Custom error types
+├── handlers/       # HTTP request handlers
+├── logger/         # Logging configuration
+├── middleware/     # HTTP middleware
+├── store/          # Database operations
+└── main.go         # Application entry point
+```
+
+### Adding New Features
+
+1. Add new models to `store/`
+2. Create handlers in `handlers/`
+3. Register routes in `app/app.go`
+4. Update configuration if needed
+5. Add tests
+
+## License
+
+[License details here]
+

+ 228 - 0
app/app.go

@@ -0,0 +1,228 @@
+package app
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	"git.linuxforward.com/byom/byom-gateway/config"
+	"git.linuxforward.com/byom/byom-gateway/handlers"
+	"git.linuxforward.com/byom/byom-gateway/logger"
+	"git.linuxforward.com/byom/byom-gateway/middleware"
+	"git.linuxforward.com/byom/byom-gateway/store"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+
+	"github.com/gin-contrib/cors"
+	"github.com/gin-gonic/gin"
+	"github.com/sirupsen/logrus"
+)
+
+type App struct {
+	entry       *logrus.Entry
+	cnf         *config.Config
+	ctx         context.Context
+	cancel      context.CancelFunc
+	dbHandler   *store.DataStore
+	userHandler *handlers.UserHandler
+	router      *gin.Engine
+}
+
+func NewApp(cnf *config.Config) (*App, error) {
+	ctx, cancel := context.WithCancel(context.Background())
+
+	// Create logger
+	entry := logger.NewLogger("app")
+
+	// Create app instance
+	app := &App{
+		entry:  entry,
+		cnf:    cnf,
+		ctx:    ctx,
+		cancel: cancel,
+	}
+
+	// Initialize services
+	if err := app.initServices(); err != nil {
+		cancel()
+		return nil, fmt.Errorf("failed to initialize services: %w", err)
+	}
+
+	// Initialize router and middleware
+	if err := app.initRouter(); err != nil {
+		cancel()
+		return nil, fmt.Errorf("init router: %w", err)
+	}
+
+	return app, nil
+}
+
+func (a *App) initRouter() error {
+	rtr := gin.Default()
+
+	coreRtr := rtr.Group("/api/v1/gw")
+
+	// Add core middleware
+	coreRtr.Use(middleware.Recovery(a.entry.Logger))
+	coreRtr.Use(middleware.RequestID())
+	coreRtr.Use(middleware.RequestLogger(a.entry.Logger))
+	coreRtr.Use(middleware.ErrorHandler())
+
+	// Add security middleware
+	coreRtr.Use(middleware.SecurityHeaders())
+	coreRtr.Use(middleware.RequestSanitizer())
+
+	// Configure request timeout
+	timeout := 30 * time.Second
+	coreRtr.Use(middleware.TimeoutMiddleware(timeout))
+
+	//Configure CORS
+
+	coreRtr.Use(cors.New(cors.Config{
+		AllowOrigins:     []string{"http://172.27.28.86:5173"},
+		AllowMethods:     []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
+		AllowHeaders:     []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"},
+		ExposeHeaders:    []string{"Content-Length", "Content-Type", "X-Request-ID", "Location"},
+		AllowCredentials: true,
+	}))
+
+	// Add health check endpoint
+	coreRtr.GET("/health", func(c *gin.Context) {
+		c.JSON(200, gin.H{"status": "ok"})
+	})
+
+	// Add user routes
+	// userGroup := rtr.Group("/user", gin.BasicAuth(gin.Accounts{
+	// 	a.cnf.BasicAuth.Username: a.cnf.BasicAuth.Password,
+	// }))
+	userGroup := coreRtr.Group("/user")
+	{
+		userGroup.GET("/host", a.userHandler.GetUserHost)
+		userGroup.POST("/host", a.userHandler.AddUserHost)
+		userGroup.DELETE("/host", a.userHandler.DeleteUserHost)
+		userGroup.PUT("/host", a.userHandler.UpdateUserHost)
+	}
+
+	a.router = rtr
+
+	return nil
+}
+
+func (a *App) initServices() error {
+	// Initialize database
+	dbConn, err := initDBConn(&a.cnf.Database)
+	if err != nil {
+		return fmt.Errorf("init database: %w", err)
+	}
+
+	dbHandler := store.NewDataStore(dbConn)
+	a.dbHandler = dbHandler
+
+	// Initialize user handler
+	userHandler := handlers.NewUserHandler(dbHandler, a.cnf.Homepage)
+	a.userHandler = userHandler
+
+	return nil
+}
+
+func initDBConn(cnf *config.DatabaseConfig) (*gorm.DB, error) {
+
+	db, err := gorm.Open(sqlite.Open(cnf.File), &gorm.Config{})
+	if err != nil {
+		return nil, fmt.Errorf("database: %w", err)
+	}
+
+	//Tune the DB
+	sqlDB, err := db.DB()
+	if err != nil {
+		return nil, fmt.Errorf("database: %w", err)
+	}
+
+	sqlDB.SetMaxOpenConns(cnf.MaxOpenConns)
+	sqlDB.SetMaxIdleConns(cnf.MaxIdleConns)
+	sqlDB.SetConnMaxLifetime(time.Duration(cnf.ConnMaxLifetime) * time.Second)
+
+	for _, pragma := range cnf.Pragma {
+		tx := db.Exec("PRAGMA " + pragma)
+		if tx.Error != nil {
+			return nil, fmt.Errorf("database: %w", tx.Error)
+		}
+	}
+
+	return db, nil
+}
+
+func (a *App) Run() error {
+	a.entry.Info("Starting server...")
+
+	// Configure server with timeouts
+	srv := &http.Server{
+		Addr:         ":" + a.cnf.Server.ListeningPort,
+		Handler:      a.router,
+		ReadTimeout:  15 * time.Second,
+		WriteTimeout: 15 * time.Second,
+		IdleTimeout:  60 * time.Second,
+	}
+
+	// Start server
+	go func() {
+		var err error
+		if a.cnf.Server.TlsConfig.Enabled {
+			a.entry.Infof("Starting server on port %s with TLS", a.cnf.Server.ListeningPort)
+			err = srv.ListenAndServeTLS(
+				a.cnf.Server.TlsConfig.CertFile,
+				a.cnf.Server.TlsConfig.KeyFile,
+			)
+		} else {
+			a.entry.Infof("Starting server on port %s without TLS", a.cnf.Server.ListeningPort)
+			err = srv.ListenAndServe()
+		}
+
+		if err != nil && err != http.ErrServerClosed {
+			a.entry.WithError(err).Error("Server error")
+			// Signal shutdown on critical error
+			a.cancel()
+		}
+	}()
+
+	// Graceful shutdown
+	quit := make(chan os.Signal, 1)
+	signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
+
+	select {
+	case <-quit:
+		a.entry.Info("Shutdown signal received...")
+	case <-a.ctx.Done():
+		a.entry.Info("Server error occurred, initiating shutdown...")
+	}
+
+	// Create shutdown context with timeout
+	ctxShutdown, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	// Shutdown server
+	if err := srv.Shutdown(ctxShutdown); err != nil {
+		a.entry.WithError(err).Error("Server forced to shutdown")
+		return fmt.Errorf("server forced to shutdown: %w", err)
+	}
+
+	// Close all services
+	if err := a.Close(); err != nil {
+		a.entry.WithError(err).Error("Error during service cleanup")
+		return fmt.Errorf("service cleanup error: %w", err)
+	}
+
+	a.entry.Info("Server stopped gracefully")
+	return nil
+}
+
+func (a *App) Close() error {
+	if err := a.dbHandler.Close(); err != nil {
+		return err
+	}
+	return nil
+}

+ 21 - 0
config.sample.yaml

@@ -0,0 +1,21 @@
+server:
+  port: "8080"
+  tls:
+    enabled: false
+    cert: ""
+    key: ""
+
+database:
+  file: "./data/dev.db"
+  log_mode: true
+  max_open_conns: 10
+  max_idle_conns: 5
+  conn_max_lifetime: 3600
+  pragma:
+    - "journal_mode=WAL"
+    - "busy_timeout=5000"
+    - "foreign_keys=ON"
+
+basic_auth:
+  username: "admin"
+  password: "dev_password"

+ 135 - 0
config/config.go

@@ -0,0 +1,135 @@
+package config
+
+import (
+	"fmt"
+	"os"
+
+	"gopkg.in/yaml.v3"
+)
+
+type Config struct {
+	Server    ServerConfig   `yaml:"server"`
+	Database  DatabaseConfig `yaml:"database"`
+	BasicAuth BasicAuth      `yaml:"basic_auth"`
+	Homepage  string         `yaml:"homepage"`
+}
+
+func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	type rawConfig Config
+	raw := rawConfig{
+		Server:   ServerConfig{},
+		Database: DatabaseConfig{},
+	}
+
+	if err := unmarshal(&raw); err != nil {
+		return err
+	}
+
+	*c = Config(raw)
+	return nil
+}
+
+type ServerConfig struct {
+	ListeningPort string    `yaml:"port"`
+	TlsConfig     TlsConfig `yaml:"tls"`
+}
+
+func (c *ServerConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	type rawServerConfig ServerConfig
+	raw := rawServerConfig{
+		TlsConfig: TlsConfig{},
+	}
+
+	if err := unmarshal(&raw); err != nil {
+		return err
+	}
+
+	*c = ServerConfig(raw)
+	return nil
+}
+
+type DatabaseConfig struct {
+	File            string   `yaml:"file"`
+	LogMode         bool     `yaml:"log_mode"`
+	MaxOpenConns    int      `yaml:"max_open_conns"`
+	MaxIdleConns    int      `yaml:"max_idle_conns"`
+	ConnMaxLifetime int      `yaml:"conn_max_lifetime"`
+	Pragma          []string `yaml:"pragma"`
+}
+
+func (c *DatabaseConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	type rawDatabaseConfig DatabaseConfig
+	raw := rawDatabaseConfig{}
+
+	if err := unmarshal(&raw); err != nil {
+		return err
+	}
+
+	if raw.File == "" {
+		return fmt.Errorf("database file not provided")
+	}
+
+	*c = DatabaseConfig(raw)
+	return nil
+}
+
+type TlsConfig struct {
+	Enabled  bool   `yaml:"enabled"`
+	CertFile string `yaml:"cert"`
+	KeyFile  string `yaml:"key"`
+}
+
+func (c *TlsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	type rawTlsConfig TlsConfig
+	raw := rawTlsConfig{
+		Enabled: false,
+	}
+
+	if err := unmarshal(&raw); err != nil {
+		return err
+	}
+
+	if raw.Enabled && (raw.CertFile == "" || raw.KeyFile == "") {
+		return fmt.Errorf("tls enabled but cert or key not provided")
+	}
+
+	*c = TlsConfig(raw)
+	return nil
+}
+
+type BasicAuth struct {
+	Username string `yaml:"username"`
+	Password string `yaml:"password"`
+}
+
+func (c *BasicAuth) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	type rawBasicAuth BasicAuth
+	raw := rawBasicAuth{}
+
+	if err := unmarshal(&raw); err != nil {
+		return err
+	}
+
+	if raw.Username == "" || raw.Password == "" {
+		return fmt.Errorf("username or password not provided")
+	}
+
+	*c = BasicAuth(raw)
+	return nil
+}
+
+func ReadConfig(cfgPath string) (*Config, error) {
+	cfg, err := os.ReadFile(cfgPath)
+	if err != nil {
+		return nil, err
+	}
+
+	var config Config
+	if err := yaml.Unmarshal(cfg, &config); err != nil {
+		return nil, err
+	}
+
+	fmt.Println(config)
+
+	return &config, nil
+}

+ 143 - 0
errors/errors.go

@@ -0,0 +1,143 @@
+package errors
+
+import (
+	"fmt"
+	"net/http"
+)
+
+// ErrorType represents the type of error
+type ErrorType string
+
+const (
+	ErrorTypeValidation     ErrorType = "VALIDATION_ERROR"
+	ErrorTypeAuthorization  ErrorType = "AUTHORIZATION_ERROR"
+	ErrorTypeAuthentication ErrorType = "AUTHENTICATION_ERROR"
+	ErrorTypeNotFound       ErrorType = "NOT_FOUND"
+	ErrorTypeConflict       ErrorType = "CONFLICT"
+	ErrorTypeInternal       ErrorType = "INTERNAL_ERROR"
+	ErrorTypeBadRequest     ErrorType = "BAD_REQUEST"
+)
+
+// AppError represents an application error
+type AppError struct {
+	Type    ErrorType `json:"type"`
+	Message string    `json:"message"`
+	Detail  string    `json:"detail,omitempty"`
+	Field   string    `json:"field,omitempty"`
+	Code    int       `json:"-"`
+	Err     error     `json:"-"`
+}
+
+func (e *AppError) Error() string {
+	if e.Detail != "" {
+		return fmt.Sprintf("%s: %s (%s)", e.Type, e.Message, e.Detail)
+	}
+	return fmt.Sprintf("%s: %s", e.Type, e.Message)
+}
+
+func (e *AppError) Unwrap() error {
+	return e.Err
+}
+
+// Error constructors
+func NewValidationError(message, field string) *AppError {
+	return &AppError{
+		Type:    ErrorTypeValidation,
+		Message: message,
+		Field:   field,
+		Code:    http.StatusBadRequest,
+	}
+}
+
+func NewAuthorizationError(message string) *AppError {
+	return &AppError{
+		Type:    ErrorTypeAuthorization,
+		Message: message,
+		Code:    http.StatusForbidden,
+	}
+}
+
+func NewAuthenticationError(message string) *AppError {
+	return &AppError{
+		Type:    ErrorTypeAuthentication,
+		Message: message,
+		Code:    http.StatusUnauthorized,
+	}
+}
+
+func NewNotFoundError(resource, id string) *AppError {
+	return &AppError{
+		Type:    ErrorTypeNotFound,
+		Message: fmt.Sprintf("%s not found", resource),
+		Detail:  fmt.Sprintf("ID: %s", id),
+		Code:    http.StatusNotFound,
+	}
+}
+
+func NewConflictError(message, detail string) *AppError {
+	return &AppError{
+		Type:    ErrorTypeConflict,
+		Message: message,
+		Detail:  detail,
+		Code:    http.StatusConflict,
+	}
+}
+
+func NewInternalError(message string, err error) *AppError {
+	return &AppError{
+		Type:    ErrorTypeInternal,
+		Message: message,
+		Detail:  err.Error(),
+		Code:    http.StatusInternalServerError,
+		Err:     err,
+	}
+}
+
+// Error type checks
+func IsNotFound(err error) bool {
+	if err == nil {
+		return false
+	}
+	if e, ok := err.(*AppError); ok {
+		return e.Type == ErrorTypeNotFound
+	}
+	return false
+}
+
+func IsValidation(err error) bool {
+	if err == nil {
+		return false
+	}
+	if e, ok := err.(*AppError); ok {
+		return e.Type == ErrorTypeValidation
+	}
+	return false
+}
+
+func IsAuthorization(err error) bool {
+	if err == nil {
+		return false
+	}
+	if e, ok := err.(*AppError); ok {
+		return e.Type == ErrorTypeAuthorization || e.Type == ErrorTypeAuthentication
+	}
+	return false
+}
+
+// WithDetail adds detail to an existing error
+func WithDetail(err *AppError, detail string) *AppError {
+	if err == nil {
+		return nil
+	}
+	err.Detail = detail
+	return err
+}
+
+// WithField adds a field name to an existing error
+func WithField(err *AppError, field string) *AppError {
+	if err == nil {
+		return nil
+	}
+	err.Field = field
+	return err
+}

+ 45 - 0
go.mod

@@ -0,0 +1,45 @@
+module git.linuxforward.com/byom/byom-gateway
+
+go 1.22.5
+
+require github.com/sirupsen/logrus v1.9.3
+
+require (
+	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/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.23.0 // indirect
+	github.com/goccy/go-json v0.10.4 // 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.9 // indirect
+	github.com/leodido/go-urn v1.4.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-sqlite3 v1.14.22 // 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.3 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	github.com/ugorji/go/codec v1.2.12 // indirect
+	golang.org/x/arch v0.12.0 // indirect
+	golang.org/x/crypto v0.31.0 // indirect
+	golang.org/x/net v0.33.0 // indirect
+	golang.org/x/text v0.21.0 // indirect
+	google.golang.org/protobuf v1.36.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
+	golang.org/x/sys v0.28.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1
+	gorm.io/driver/sqlite v1.5.7
+	gorm.io/gorm v1.25.12
+)

+ 126 - 0
go.sum

@@ -0,0 +1,126 @@
+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=
+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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+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/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/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/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/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/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=
+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.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+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=
+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.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+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/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+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/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+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/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+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.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
+gorm.io/driver/sqlite v1.5.7/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=

+ 106 - 0
handlers/users.go

@@ -0,0 +1,106 @@
+package handlers
+
+import (
+	"fmt"
+
+	"git.linuxforward.com/byom/byom-gateway/store"
+	"github.com/gin-gonic/gin"
+	"github.com/sirupsen/logrus"
+)
+
+type UserHandler struct {
+	entry       *logrus.Entry
+	db          *store.DataStore
+	pricingPage string
+}
+
+// this handlers is gonna be used in order to check
+// if a specified user using its email has an hostname associated
+func NewUserHandler(db *store.DataStore, pricingPage string) *UserHandler {
+	return &UserHandler{
+		entry:       logrus.WithField("handler", "user-host"),
+		db:          db,
+		pricingPage: pricingPage,
+	}
+}
+
+func (u *UserHandler) GetUserHost(c *gin.Context) {
+	email := c.Query("email")
+	if email == "" {
+		c.JSON(400, gin.H{"error": "email is required"})
+		return
+	}
+
+	host, err := u.db.GetUserHost(email)
+	fmt.Printf("Host from DB: %v, err: %v\n", host, err) // Add this log
+
+	if err != nil || host == "" {
+		c.JSON(200, gin.H{"redirect": u.pricingPage})
+		return
+	}
+
+	c.JSON(200, gin.H{"redirect": "https://" + host + "/login"})
+}
+
+func (u *UserHandler) AddUserHost(c *gin.Context) {
+	var req struct {
+		Email string `json:"email"`
+		Host  string `json:"host"`
+	}
+
+	if err := c.BindJSON(&req); err != nil {
+		c.JSON(400, gin.H{"error": err.Error()})
+		return
+	}
+
+	if req.Email == "" || req.Host == "" {
+		c.JSON(400, gin.H{"error": "email is required"})
+		return
+	}
+
+	if err := u.db.AddUserHost(req.Email, req.Host); err != nil {
+		c.JSON(500, gin.H{"error": err.Error()})
+		return
+	}
+
+	c.JSON(200, gin.H{"status": "ok"})
+}
+
+func (u *UserHandler) DeleteUserHost(c *gin.Context) {
+	email := c.Query("email")
+	if email == "" {
+		c.JSON(400, gin.H{"error": "email is required"})
+		return
+	}
+
+	if err := u.db.DeleteUserHost(email); err != nil {
+		c.JSON(500, gin.H{"error": err.Error()})
+		return
+	}
+
+	c.JSON(200, gin.H{"status": "ok"})
+}
+
+func (u *UserHandler) UpdateUserHost(c *gin.Context) {
+	var req struct {
+		Email string `json:"email"`
+		Host  string `json:"host"`
+	}
+
+	if err := c.BindJSON(&req); err != nil {
+		c.JSON(400, gin.H{"error": err.Error()})
+		return
+	}
+
+	if req.Email == "" || req.Host == "" {
+		c.JSON(400, gin.H{"error": "email is required"})
+		return
+	}
+
+	if err := u.db.UpdateUserHost(req.Email, req.Host); err != nil {
+		c.JSON(500, gin.H{"error": err.Error()})
+		return
+	}
+
+	c.JSON(200, gin.H{"status": "ok"})
+}

+ 64 - 0
logger/logger.go

@@ -0,0 +1,64 @@
+package logger
+
+import (
+	"os"
+
+	"github.com/sirupsen/logrus"
+)
+
+// Fields type for structured logging
+type Fields logrus.Fields
+
+var (
+	// defaultLogger is the default logger instance
+	defaultLogger = logrus.New()
+)
+
+func init() {
+	// Set default configuration
+	defaultLogger.SetOutput(os.Stdout)
+	defaultLogger.SetLevel(logrus.InfoLevel)
+	defaultLogger.SetFormatter(&logrus.TextFormatter{
+		FullTimestamp: true,
+	})
+}
+
+// Configure sets up the global logger configuration
+func Configure(level string, noColor, forceColors, inJSON bool) error {
+	// Set log level
+	if level != "" {
+		lvl, err := logrus.ParseLevel(level)
+		if err != nil {
+			return err
+		}
+		defaultLogger.SetLevel(lvl)
+	}
+
+	// Configure formatter
+	if inJSON {
+		defaultLogger.SetFormatter(&logrus.JSONFormatter{})
+	} else {
+		defaultLogger.SetFormatter(&logrus.TextFormatter{
+			ForceColors:   forceColors,
+			DisableColors: noColor,
+			FullTimestamp: true,
+		})
+	}
+
+	return nil
+}
+
+// NewLogger creates a new logger entry with component field
+func NewLogger(component string) *logrus.Entry {
+	return defaultLogger.WithField("component", component)
+}
+
+// WithRequestID adds request ID to the logger entry
+func WithRequestID(logger *logrus.Entry, requestID string) *logrus.Entry {
+	return logger.WithField("request_id", requestID)
+}
+
+// GetLogger returns the default logger instance
+func GetLogger() *logrus.Logger {
+	return defaultLogger
+}

+ 41 - 0
main.go

@@ -0,0 +1,41 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+
+	"git.linuxforward.com/byom/byom-gateway/app"
+	"git.linuxforward.com/byom/byom-gateway/config"
+)
+
+func main() {
+
+	cfgPath := flag.String("config", "config.yaml", "Path to the configuration file")
+
+	flag.Parse()
+
+	if *cfgPath == "" {
+		fmt.Println("Config file not provided")
+		os.Exit(1)
+	}
+
+	// Read the configuration file
+	cfg, err := config.ReadConfig(*cfgPath)
+	if err != nil {
+		fmt.Println("Error reading the configuration file: ", err)
+		os.Exit(1)
+	}
+
+	app, err := app.NewApp(cfg)
+	if err != nil {
+		fmt.Println("Error creating the app: ", err)
+		return
+	}
+
+	err = app.Run()
+	if err != nil {
+		fmt.Println("Error running the app: ", err)
+		os.Exit(1)
+	}
+}

+ 50 - 0
middleware/cors.go

@@ -0,0 +1,50 @@
+package middleware
+
+import (
+	"time"
+
+	"github.com/gin-contrib/cors"
+	"github.com/gin-gonic/gin"
+)
+
+// CORSConfig represents the configuration for CORS
+type CORSConfig struct {
+	AllowOrigins     []string
+	AllowCredentials bool
+	MaxAge           time.Duration
+	TrustedProxies   []string
+}
+
+// DefaultCORSConfig returns the default CORS configuration
+func DefaultCORSConfig() *CORSConfig {
+	return &CORSConfig{
+		AllowOrigins:     []string{"http://172.27.28.86:5173/"},
+		AllowCredentials: true,
+		MaxAge:           12 * time.Hour,
+		TrustedProxies:   []string{"localhost", "127.0.0.1"},
+	}
+}
+
+// CORS returns a middleware for handling CORS with secure defaults
+func CORS(cfg *CORSConfig) gin.HandlerFunc {
+	if cfg == nil {
+		cfg = DefaultCORSConfig()
+	}
+
+	config := cors.Config{
+		AllowOrigins:     cfg.AllowOrigins,
+		AllowMethods:     []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
+		AllowHeaders:     []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"},
+		ExposeHeaders:    []string{"Content-Length", "Content-Type", "X-Request-ID"},
+		AllowCredentials: cfg.AllowCredentials,
+		MaxAge:           cfg.MaxAge,
+	}
+
+	// Add security-focused CORS settings
+	config.AllowWildcard = true
+	config.AllowBrowserExtensions = false
+	config.AllowWebSockets = false
+	config.AllowFiles = false
+
+	return cors.New(config)
+}

+ 37 - 0
middleware/error_handler.go

@@ -0,0 +1,37 @@
+package middleware
+
+import (
+	"net/http"
+
+	"git.linuxforward.com/byom/byom-gateway/errors"
+	"github.com/gin-gonic/gin"
+)
+
+// ErrorHandler handles errors and returns appropriate responses
+func ErrorHandler() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		c.Next()
+
+		if len(c.Errors) > 0 {
+			err := c.Errors.Last().Err
+			if appErr, ok := err.(*errors.AppError); ok {
+				switch appErr.Type {
+				case errors.ErrorTypeValidation:
+					c.AbortWithStatusJSON(http.StatusBadRequest, appErr)
+				case errors.ErrorTypeNotFound:
+					c.AbortWithStatusJSON(http.StatusNotFound, appErr)
+				case errors.ErrorTypeAuthorization:
+					c.AbortWithStatusJSON(http.StatusUnauthorized, appErr)
+				case errors.ErrorTypeConflict:
+					c.AbortWithStatusJSON(http.StatusConflict, appErr)
+				default:
+					c.AbortWithStatusJSON(http.StatusInternalServerError, appErr)
+				}
+				return
+			}
+			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
+				"error": err.Error(),
+			})
+		}
+	}
+}

+ 50 - 0
middleware/recovery.go

@@ -0,0 +1,50 @@
+package middleware
+
+import (
+	"fmt"
+	"net/http"
+	"runtime/debug"
+
+	"git.linuxforward.com/byom/byom-gateway/logger"
+	"github.com/gin-gonic/gin"
+	"github.com/sirupsen/logrus"
+)
+
+// Recovery returns a middleware that recovers from panics and logs the stack trace
+func Recovery(log *logrus.Logger) gin.HandlerFunc {
+	recoveryLogger := logger.NewLogger("recovery")
+	return func(c *gin.Context) {
+		defer func() {
+			if err := recover(); err != nil {
+				// Get request ID if available
+				requestID := c.GetString(RequestIDKey)
+				if requestID == "" {
+					requestID = "no-request-id"
+				}
+
+				// Get stack trace
+				stack := string(debug.Stack())
+
+				// Add request ID to logger
+				reqLogger := logger.WithRequestID(recoveryLogger, requestID)
+
+				// Log the panic with context
+				reqLogger.WithFields(logrus.Fields{
+					"error":     fmt.Sprintf("%v", err),
+					"stack":     stack,
+					"method":    c.Request.Method,
+					"path":      c.Request.URL.Path,
+					"client_ip": c.ClientIP(),
+				}).Error("Panic recovered")
+
+				// Return a 500 error to the client
+				c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
+					"error": "Internal server error",
+					"code":  "INTERNAL_SERVER_ERROR",
+				})
+			}
+		}()
+
+		c.Next()
+	}
+}

+ 31 - 0
middleware/request_id.go

@@ -0,0 +1,31 @@
+package middleware
+
+import (
+	"context"
+
+	"github.com/gin-gonic/gin"
+	"github.com/google/uuid"
+)
+
+const RequestIDKey = "X-Request-ID"
+
+// RequestID middleware adds a unique request ID to each request
+func RequestID() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Check if request already has an ID
+		requestID := c.GetHeader(RequestIDKey)
+		if requestID == "" {
+			requestID = uuid.New().String()
+		}
+
+		// Set request ID in header and context
+		c.Header(RequestIDKey, requestID)
+		c.Set(RequestIDKey, requestID)
+
+		// Add request ID to context for logging
+		ctx := context.WithValue(c.Request.Context(), RequestIDKey, requestID)
+		c.Request = c.Request.WithContext(ctx)
+
+		c.Next()
+	}
+}

+ 55 - 0
middleware/request_logger.go

@@ -0,0 +1,55 @@
+package middleware
+
+import (
+	"fmt"
+	"time"
+
+	"git.linuxforward.com/byom/byom-gateway/logger"
+	"github.com/gin-gonic/gin"
+	"github.com/sirupsen/logrus"
+)
+
+// RequestLogger middleware logs details about each request
+func RequestLogger(log *logrus.Logger) gin.HandlerFunc {
+	requestLogger := logger.NewLogger("request")
+	return func(c *gin.Context) {
+		// Start timer
+		start := time.Now()
+
+		// Get request ID
+		requestID := c.GetString(RequestIDKey)
+		if requestID == "" {
+			requestID = "no-request-id"
+		}
+
+		// Add request ID to logger
+		reqLogger := logger.WithRequestID(requestLogger, requestID)
+
+		// Process request
+		c.Next()
+
+		// Calculate latency
+		latency := time.Since(start)
+
+		// Get path and status
+		path := c.Request.URL.Path
+		if raw := c.Request.URL.RawQuery; raw != "" {
+			path = fmt.Sprintf("%s?%s", path, raw)
+		}
+
+		// Log request details
+		reqLogger.WithFields(logrus.Fields{
+			"method":     c.Request.Method,
+			"path":       path,
+			"status":     c.Writer.Status(),
+			"latency_ms": float64(latency.Nanoseconds()) / 1e6,
+			"client_ip":  c.ClientIP(),
+			"user_agent": c.Request.UserAgent(),
+		}).Info("Request processed")
+
+		// If there was an error, log it with the error field
+		if len(c.Errors) > 0 {
+			reqLogger.WithField("errors", c.Errors.String()).Error("Request errors")
+		}
+	}
+}

+ 98 - 0
middleware/security.go

@@ -0,0 +1,98 @@
+package middleware
+
+import (
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/gin-gonic/gin"
+)
+
+// SecurityHeaders adds security-related headers to all responses
+func SecurityHeaders() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Security headers
+		c.Header("X-Content-Type-Options", "nosniff")
+		c.Header("X-Frame-Options", "DENY")
+		c.Header("X-XSS-Protection", "1; mode=block")
+		c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
+		c.Header("Content-Security-Policy", "default-src 'self'; frame-ancestors 'none'")
+		c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
+		c.Header("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
+
+		c.Next()
+	}
+}
+
+// RequestSanitizer sanitizes incoming requests
+func RequestSanitizer() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Sanitize headers
+		sanitizeHeaders(c)
+
+		// Block potentially dangerous file extensions
+		if containsDangerousExtension(c.Request.URL.Path) {
+			c.AbortWithStatus(http.StatusForbidden)
+			return
+		}
+
+		c.Next()
+	}
+}
+
+// TimeoutMiddleware adds a timeout to the request context
+func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Wrap the request in a timeout
+		ch := make(chan struct{})
+		go func() {
+			c.Next()
+			ch <- struct{}{}
+		}()
+
+		select {
+		case <-ch:
+			return
+		case <-time.After(timeout):
+			c.AbortWithStatusJSON(http.StatusGatewayTimeout, gin.H{
+				"code":  "REQUEST_TIMEOUT",
+				"error": "Request timeout exceeded",
+			})
+			return
+		}
+	}
+}
+
+// Helper functions
+func sanitizeHeaders(c *gin.Context) {
+	// Remove potentially dangerous headers
+	c.Request.Header.Del("X-Forwarded-For")
+	c.Request.Header.Del("X-Real-IP")
+	c.Request.Header.Del("X-Forwarded-Proto")
+
+	// Sanitize User-Agent
+	if ua := c.Request.Header.Get("User-Agent"); ua != "" {
+		c.Request.Header.Set("User-Agent", sanitizeString(ua))
+	}
+}
+
+func containsDangerousExtension(path string) bool {
+	dangerous := []string{".php", ".asp", ".aspx", ".jsp", ".cgi", ".exe", ".bat", ".cmd", ".sh", ".pl"}
+	path = strings.ToLower(path)
+	for _, ext := range dangerous {
+		if strings.HasSuffix(path, ext) {
+			return true
+		}
+	}
+	return false
+}
+
+func sanitizeString(s string) string {
+	// Remove any control characters
+	return strings.Map(func(r rune) rune {
+		if r < 32 || r == 127 {
+			return -1
+		}
+		return r
+	}, s)
+}

+ 61 - 0
store/interfaces.go

@@ -0,0 +1,61 @@
+package store
+
+import (
+	"fmt"
+
+	"git.linuxforward.com/byom/byom-gateway/logger"
+	"github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+type DataStore struct {
+	logger *logrus.Entry
+	db     *gorm.DB
+}
+
+func NewDataStore(db *gorm.DB) *DataStore {
+
+	err := InitTables(db)
+	if err != nil {
+		logger.NewLogger("data-store").Fatal("Failed to initialize tables: ", err)
+	}
+
+	return &DataStore{
+		db:     db,
+		logger: logger.NewLogger("data-store"),
+	}
+}
+
+func InitTables(db *gorm.DB) error {
+	err := db.AutoMigrate(&UserHost{})
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+//Tx methods
+
+func (s *DataStore) BeginTx() *gorm.DB {
+	return s.db.Begin()
+}
+
+func (s *DataStore) CommitTx(tx *gorm.DB) error {
+	return tx.Commit().Error
+}
+
+func (s *DataStore) Close() error {
+	s.logger.Info("Closing database connections")
+
+	sqlDB, err := s.db.DB()
+	if err != nil {
+		return fmt.Errorf("get sql.DB instance: %w", err)
+	}
+
+	if err := sqlDB.Close(); err != nil {
+		return fmt.Errorf("close database connections: %w", err)
+	}
+
+	s.logger.Info("Database connections closed successfully")
+	return nil
+}

+ 41 - 0
store/users.go

@@ -0,0 +1,41 @@
+package store
+
+import "github.com/google/uuid"
+
+type UserHost struct {
+	ID    uuid.UUID `gorm:"type:uuid;primary_key" json:"id"`
+	Email string    `gorm:"unique;size:255" json:"email"`
+	Host  string    `gorm:"size:255" json:"host"`
+}
+
+func (UserHost) TableName() string {
+	return "user_hosts"
+}
+
+// GetUserHost returns the host associated with the given email
+func (s *DataStore) GetUserHost(email string) (string, error) {
+	var user UserHost
+	if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
+		return "", err
+	}
+	return user.Host, nil
+}
+
+// AddUserHost adds a new user with the given email and host
+func (s *DataStore) AddUserHost(email, host string) error {
+	user := UserHost{
+		Email: email,
+		Host:  host,
+	}
+	return s.db.Create(&user).Error
+}
+
+// DeleteUserHost deletes the user with the given email
+func (s *DataStore) DeleteUserHost(email string) error {
+	return s.db.Where("email = ?", email).Delete(UserHost{}).Error
+}
+
+// UpdateUserHost updates the host associated with the given email
+func (s *DataStore) UpdateUserHost(email, host string) error {
+	return s.db.Model(UserHost{}).Where("email = ?", email).Update("host", host).Error
+}

+ 0 - 0
test.json