UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

1,680 lines (1,453 loc) 65.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.chiTemplate = void 0; exports.chiTemplate = { id: 'chi', name: 'chi', displayName: 'Chi Router', description: 'Lightweight, idiomatic and composable router for building Go HTTP services', language: 'go', framework: 'chi', version: '5.0.11', tags: ['go', 'chi', 'api', 'rest', 'router', 'middleware', 'composable'], port: 8080, dependencies: {}, features: ['authentication', 'validation', 'logging', 'cors', 'documentation', 'rate-limiting'], files: { // Go module configuration 'go.mod': `module {{projectName}} go 1.21 require ( github.com/go-chi/chi/v5 v5.0.11 github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.8.0 github.com/go-chi/jwtauth/v5 v5.3.0 github.com/go-chi/render v1.0.3 github.com/go-chi/httplog v0.3.2 github.com/joho/godotenv v1.5.1 github.com/go-playground/validator/v10 v10.16.0 github.com/swaggo/swag v1.16.2 github.com/swaggo/http-swagger/v2 v2.0.2 github.com/rs/zerolog v1.31.0 github.com/redis/go-redis/v9 v9.3.1 gorm.io/gorm v1.25.5 gorm.io/driver/postgres v1.5.4 gorm.io/driver/mysql v1.5.2 gorm.io/driver/sqlite v1.5.4 golang.org/x/crypto v0.17.0 github.com/google/uuid v1.5.0 github.com/lestrrat-go/jwx/v2 v2.0.19 ) require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/PuerkitoBio/purell v1.2.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/ajg/form v1.5.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect github.com/go-openapi/spec v0.20.13 // indirect github.com/go-openapi/swag v0.22.7 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jackc/pgx/v5 v5.5.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.19 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.16.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) `, // Main application entry point 'main.go': `package main import ( "context" "fmt" "net/http" "os" "os/signal" "syscall" "time" "{{projectName}}/config" "{{projectName}}/database" _ "{{projectName}}/docs" // swagger docs "{{projectName}}/handlers" "{{projectName}}/middleware" "{{projectName}}/routes" "github.com/go-chi/chi/v5" chiMiddleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-chi/httplog" "github.com/joho/godotenv" "github.com/rs/zerolog" "github.com/rs/zerolog/log" httpSwagger "github.com/swaggo/http-swagger/v2" ) // @title {{projectName}} API // @version 1.0 // @description API server for {{projectName}} built with Chi router // @termsOfService http://swagger.io/terms/ // @contact.name API Support // @contact.url http://www.swagger.io/support // @contact.email support@swagger.io // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @host localhost:8080 // @BasePath /api/v1 // @securityDefinitions.apikey Bearer // @in header // @name Authorization // @description Type "Bearer" followed by a space and JWT token. func main() { // Load environment variables if err := godotenv.Load(); err != nil { log.Info().Msg("No .env file found") } // Initialize configuration cfg := config.New() // Initialize logger setupLogger(cfg) // Initialize database db, err := database.Initialize(cfg) if err != nil { log.Fatal().Err(err).Msg("Failed to initialize database") } // Run migrations if err := database.Migrate(db); err != nil { log.Fatal().Err(err).Msg("Failed to run migrations") } // Create router r := chi.NewRouter() // Global middleware r.Use(chiMiddleware.RequestID) r.Use(chiMiddleware.RealIP) r.Use(httplog.RequestLogger(httplog.NewLogger("{{projectName}}", httplog.Options{ JSON: cfg.Environment == "production", Concise: cfg.Environment == "production", RequestHeaders: cfg.Environment == "development", MessageFieldName: "msg", TimeFieldFormat: time.RFC3339, Tags: map[string]string{ "version": "1.0.0", "env": cfg.Environment, }, }))) r.Use(chiMiddleware.Recoverer) r.Use(chiMiddleware.Timeout(60 * time.Second)) r.Use(chiMiddleware.Compress(5)) r.Use(cors.Handler(cors.Options{ AllowedOrigins: cfg.AllowedOrigins, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "X-Request-ID"}, ExposedHeaders: []string{"Link", "X-Request-ID"}, AllowCredentials: true, MaxAge: 300, })) r.Use(middleware.RateLimiter(cfg)) r.Use(chiMiddleware.Heartbeat("/ping")) // Health check r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(\`{"status":"healthy","time":"\` + time.Now().UTC().Format(time.RFC3339) + \`"}\`)) }) // Swagger documentation r.Get("/swagger/*", httpSwagger.Handler( httpSwagger.URL("/swagger/doc.json"), httpSwagger.DeepLinking(true), httpSwagger.DocExpansion("none"), httpSwagger.DomID("swagger-ui"), )) // Initialize handlers h := handlers.NewHandler(db, cfg) // API routes r.Route("/api/v1", func(r chi.Router) { routes.RegisterRoutes(r, h, cfg) }) // Custom 404 handler r.NotFound(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) w.Write([]byte(\`{"error":"Resource not found"}\`)) }) // Custom 405 handler r.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte(\`{"error":"Method not allowed"}\`)) }) // Create HTTP server srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.Port), Handler: r, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } // Start server in goroutine go func() { log.Info().Msgf("Starting server on port %d", cfg.Port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal().Err(err).Msg("Failed to start server") } }() // Wait for interrupt signal quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Info().Msg("Shutting down server...") // Graceful shutdown with timeout ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatal().Err(err).Msg("Server forced to shutdown") } log.Info().Msg("Server exited") } func setupLogger(cfg *config.Config) { // Configure zerolog zerolog.TimeFieldFormat = time.RFC3339 zerolog.SetGlobalLevel(zerolog.InfoLevel) if cfg.Environment == "development" { zerolog.SetGlobalLevel(zerolog.DebugLevel) log.Logger = log.Output(zerolog.ConsoleWriter{ Out: os.Stdout, TimeFormat: time.RFC3339, }) } } `, // Configuration 'config/config.go': `package config import ( "os" "strconv" "strings" "time" ) type Config struct { Environment string Port int // Database DBHost string DBPort int DBUser string DBPassword string DBName string DBSSLMode string // JWT JWTSecret string JWTExpirationHours int // Redis RedisAddr string RedisPassword string RedisDB int // Rate limiting RateLimitRequests int RateLimitDuration time.Duration // CORS AllowedOrigins []string // Logging LogLevel string LogJSON bool } func New() *Config { return &Config{ Environment: getEnv("ENVIRONMENT", "development"), Port: getEnvAsInt("PORT", 8080), // Database DBHost: getEnv("DB_HOST", "localhost"), DBPort: getEnvAsInt("DB_PORT", 5432), DBUser: getEnv("DB_USER", "postgres"), DBPassword: getEnv("DB_PASSWORD", "password"), DBName: getEnv("DB_NAME", "{{projectName}}"), DBSSLMode: getEnv("DB_SSLMODE", "disable"), // JWT JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-this"), JWTExpirationHours: getEnvAsInt("JWT_EXPIRATION_HOURS", 24), // Redis RedisAddr: getEnv("REDIS_ADDR", "localhost:6379"), RedisPassword: getEnv("REDIS_PASSWORD", ""), RedisDB: getEnvAsInt("REDIS_DB", 0), // Rate limiting RateLimitRequests: getEnvAsInt("RATE_LIMIT_REQUESTS", 100), RateLimitDuration: time.Duration(getEnvAsInt("RATE_LIMIT_DURATION_MINUTES", 1)) * time.Minute, // CORS AllowedOrigins: strings.Split(getEnv("ALLOWED_ORIGINS", "*"), ","), // Logging LogLevel: getEnv("LOG_LEVEL", "info"), LogJSON: getEnvAsBool("LOG_JSON", false), } } func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } func getEnvAsInt(key string, defaultValue int) int { if value := os.Getenv(key); value != "" { if intValue, err := strconv.Atoi(value); err == nil { return intValue } } return defaultValue } func getEnvAsBool(key string, defaultValue bool) bool { if value := os.Getenv(key); value != "" { if boolValue, err := strconv.ParseBool(value); err == nil { return boolValue } } return defaultValue } `, // Database 'database/database.go': `package database import ( "fmt" "time" "{{projectName}}/config" "{{projectName}}/models" "github.com/rs/zerolog/log" "gorm.io/driver/mysql" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) func Initialize(cfg *config.Config) (*gorm.DB, error) { var dialector gorm.Dialector switch cfg.DBHost { case "sqlite", ":memory:": dialector = sqlite.Open(cfg.DBName) case "mysql": dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", cfg.DBUser, cfg.DBPassword, cfg.DBHost, cfg.DBPort, cfg.DBName) dialector = mysql.Open(dsn) default: dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", cfg.DBHost, cfg.DBPort, cfg.DBUser, cfg.DBPassword, cfg.DBName, cfg.DBSSLMode) dialector = postgres.Open(dsn) } // Configure GORM gormConfig := &gorm.Config{ NowFunc: func() time.Time { return time.Now().UTC() }, PrepareStmt: true, } if cfg.Environment == "development" { gormConfig.Logger = logger.Default.LogMode(logger.Info) } else { gormConfig.Logger = logger.Default.LogMode(logger.Silent) } db, err := gorm.Open(dialector, gormConfig) if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) } // Configure connection pool sqlDB, err := db.DB() if err != nil { return nil, err } sqlDB.SetMaxIdleConns(10) sqlDB.SetMaxOpenConns(100) sqlDB.SetConnMaxLifetime(time.Hour) sqlDB.SetConnMaxIdleTime(10 * time.Minute) log.Info().Msg("Database connection established") return db, nil } func Migrate(db *gorm.DB) error { return db.AutoMigrate( &models.User{}, &models.Product{}, &models.Order{}, &models.OrderItem{}, ) } `, // Models 'models/user.go': `package models import ( "time" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) type User struct { ID uint \`gorm:"primarykey" json:"id"\` CreatedAt time.Time \`json:"created_at"\` UpdatedAt time.Time \`json:"updated_at"\` DeletedAt gorm.DeletedAt \`gorm:"index" json:"-"\` Email string \`gorm:"uniqueIndex;not null" json:"email" validate:"required,email"\` Password string \`gorm:"not null" json:"-"\` FirstName string \`gorm:"not null" json:"first_name" validate:"required,min=2,max=50"\` LastName string \`gorm:"not null" json:"last_name" validate:"required,min=2,max=50"\` Phone string \`json:"phone,omitempty" validate:"omitempty,e164"\` Role string \`gorm:"default:user" json:"role" validate:"omitempty,oneof=user admin manager"\` Active bool \`gorm:"default:true" json:"active"\` Verified bool \`gorm:"default:false" json:"verified"\` Orders []Order \`gorm:"foreignKey:UserID" json:"-"\` } func (u *User) SetPassword(password string) error { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err } u.Password = string(hashedPassword) return nil } func (u *User) CheckPassword(password string) bool { err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) return err == nil } func (u *User) FullName() string { return u.FirstName + " " + u.LastName } type LoginRequest struct { Email string \`json:"email" validate:"required,email"\` Password string \`json:"password" validate:"required,min=6"\` } type RegisterRequest struct { Email string \`json:"email" validate:"required,email"\` Password string \`json:"password" validate:"required,min=6,max=72"\` FirstName string \`json:"first_name" validate:"required,min=2,max=50"\` LastName string \`json:"last_name" validate:"required,min=2,max=50"\` Phone string \`json:"phone,omitempty" validate:"omitempty,e164"\` } type UpdateUserRequest struct { FirstName *string \`json:"first_name,omitempty" validate:"omitempty,min=2,max=50"\` LastName *string \`json:"last_name,omitempty" validate:"omitempty,min=2,max=50"\` Phone *string \`json:"phone,omitempty" validate:"omitempty,e164"\` } type ChangePasswordRequest struct { CurrentPassword string \`json:"current_password" validate:"required"\` NewPassword string \`json:"new_password" validate:"required,min=6,max=72"\` } type UserResponse struct { ID uint \`json:"id"\` Email string \`json:"email"\` FirstName string \`json:"first_name"\` LastName string \`json:"last_name"\` Phone string \`json:"phone,omitempty"\` Role string \`json:"role"\` Active bool \`json:"active"\` Verified bool \`json:"verified"\` CreatedAt time.Time \`json:"created_at"\` } func (u *User) ToResponse() UserResponse { return UserResponse{ ID: u.ID, Email: u.Email, FirstName: u.FirstName, LastName: u.LastName, Phone: u.Phone, Role: u.Role, Active: u.Active, Verified: u.Verified, CreatedAt: u.CreatedAt, } } `, 'models/product.go': `package models import ( "time" "gorm.io/gorm" ) type Product struct { ID uint \`gorm:"primarykey" json:"id"\` CreatedAt time.Time \`json:"created_at"\` UpdatedAt time.Time \`json:"updated_at"\` DeletedAt gorm.DeletedAt \`gorm:"index" json:"-"\` Name string \`gorm:"not null;index" json:"name" validate:"required,min=1,max=200"\` Description string \`json:"description" validate:"max=1000"\` Price float64 \`gorm:"not null;check:price >= 0" json:"price" validate:"required,min=0"\` Cost float64 \`gorm:"not null;check:cost >= 0" json:"cost" validate:"required,min=0"\` Stock int \`gorm:"default:0;check:stock >= 0" json:"stock" validate:"min=0"\` SKU string \`gorm:"uniqueIndex;not null" json:"sku" validate:"required,min=3,max=50"\` Barcode string \`gorm:"uniqueIndex" json:"barcode,omitempty" validate:"omitempty,min=8,max=13"\` Category string \`gorm:"index;not null" json:"category" validate:"required,min=1,max=100"\` Brand string \`gorm:"index" json:"brand,omitempty" validate:"omitempty,max=100"\` Weight float64 \`json:"weight,omitempty" validate:"omitempty,min=0"\` Dimensions string \`json:"dimensions,omitempty" validate:"omitempty,max=100"\` Active bool \`gorm:"default:true;index" json:"active"\` Featured bool \`gorm:"default:false;index" json:"featured"\` OrderItems []OrderItem \`gorm:"foreignKey:ProductID" json:"-"\` } type CreateProductRequest struct { Name string \`json:"name" validate:"required,min=1,max=200"\` Description string \`json:"description" validate:"max=1000"\` Price float64 \`json:"price" validate:"required,min=0"\` Cost float64 \`json:"cost" validate:"required,min=0"\` Stock int \`json:"stock" validate:"min=0"\` SKU string \`json:"sku" validate:"required,min=3,max=50"\` Barcode string \`json:"barcode,omitempty" validate:"omitempty,min=8,max=13"\` Category string \`json:"category" validate:"required,min=1,max=100"\` Brand string \`json:"brand,omitempty" validate:"omitempty,max=100"\` Weight float64 \`json:"weight,omitempty" validate:"omitempty,min=0"\` Dimensions string \`json:"dimensions,omitempty" validate:"omitempty,max=100"\` } type UpdateProductRequest struct { Name *string \`json:"name,omitempty" validate:"omitempty,min=1,max=200"\` Description *string \`json:"description,omitempty" validate:"omitempty,max=1000"\` Price *float64 \`json:"price,omitempty" validate:"omitempty,min=0"\` Cost *float64 \`json:"cost,omitempty" validate:"omitempty,min=0"\` Stock *int \`json:"stock,omitempty" validate:"omitempty,min=0"\` Category *string \`json:"category,omitempty" validate:"omitempty,min=1,max=100"\` Brand *string \`json:"brand,omitempty" validate:"omitempty,max=100"\` Weight *float64 \`json:"weight,omitempty" validate:"omitempty,min=0"\` Dimensions *string \`json:"dimensions,omitempty" validate:"omitempty,max=100"\` Active *bool \`json:"active,omitempty"\` Featured *bool \`json:"featured,omitempty"\` } type ProductListRequest struct { Page int \`json:"page" validate:"min=1"\` Limit int \`json:"limit" validate:"min=1,max=100"\` Search string \`json:"search,omitempty" validate:"omitempty,min=1,max=100"\` Category string \`json:"category,omitempty" validate:"omitempty,min=1,max=100"\` Brand string \`json:"brand,omitempty" validate:"omitempty,min=1,max=100"\` MinPrice float64 \`json:"min_price,omitempty" validate:"omitempty,min=0"\` MaxPrice float64 \`json:"max_price,omitempty" validate:"omitempty,min=0,gtefield=MinPrice"\` InStock bool \`json:"in_stock,omitempty"\` Featured bool \`json:"featured,omitempty"\` SortBy string \`json:"sort_by,omitempty" validate:"omitempty,oneof=name price created_at stock"\` SortOrder string \`json:"sort_order,omitempty" validate:"omitempty,oneof=asc desc"\` } `, 'models/order.go': `package models import ( "time" "gorm.io/gorm" ) type OrderStatus string const ( OrderStatusPending OrderStatus = "pending" OrderStatusProcessing OrderStatus = "processing" OrderStatusShipped OrderStatus = "shipped" OrderStatusDelivered OrderStatus = "delivered" OrderStatusCancelled OrderStatus = "cancelled" OrderStatusRefunded OrderStatus = "refunded" ) type Order struct { ID uint \`gorm:"primarykey" json:"id"\` CreatedAt time.Time \`json:"created_at"\` UpdatedAt time.Time \`json:"updated_at"\` DeletedAt gorm.DeletedAt \`gorm:"index" json:"-"\` OrderNumber string \`gorm:"uniqueIndex;not null" json:"order_number"\` UserID uint \`gorm:"not null;index" json:"user_id"\` Status OrderStatus \`gorm:"default:pending;index" json:"status"\` Subtotal float64 \`gorm:"not null" json:"subtotal"\` Tax float64 \`gorm:"not null" json:"tax"\` Shipping float64 \`gorm:"not null" json:"shipping"\` Total float64 \`gorm:"not null" json:"total"\` Currency string \`gorm:"default:USD" json:"currency"\` PaymentMethod string \`json:"payment_method,omitempty"\` PaymentStatus string \`gorm:"default:pending" json:"payment_status"\` Notes string \`json:"notes,omitempty"\` // Shipping info ShippingName string \`json:"shipping_name"\` ShippingAddress string \`json:"shipping_address"\` ShippingCity string \`json:"shipping_city"\` ShippingState string \`json:"shipping_state"\` ShippingZip string \`json:"shipping_zip"\` ShippingCountry string \`json:"shipping_country"\` User User \`gorm:"foreignKey:UserID" json:"user,omitempty"\` OrderItems []OrderItem \`gorm:"foreignKey:OrderID" json:"items,omitempty"\` } type OrderItem struct { ID uint \`gorm:"primarykey" json:"id"\` CreatedAt time.Time \`json:"created_at"\` UpdatedAt time.Time \`json:"updated_at"\` DeletedAt gorm.DeletedAt \`gorm:"index" json:"-"\` OrderID uint \`gorm:"not null;index" json:"order_id"\` ProductID uint \`gorm:"not null;index" json:"product_id"\` Quantity int \`gorm:"not null;check:quantity > 0" json:"quantity"\` Price float64 \`gorm:"not null" json:"price"\` Total float64 \`gorm:"not null" json:"total"\` Product Product \`gorm:"foreignKey:ProductID" json:"product,omitempty"\` } type CreateOrderRequest struct { Items []struct { ProductID uint \`json:"product_id" validate:"required"\` Quantity int \`json:"quantity" validate:"required,min=1"\` } \`json:"items" validate:"required,min=1,dive"\` ShippingName string \`json:"shipping_name" validate:"required"\` ShippingAddress string \`json:"shipping_address" validate:"required"\` ShippingCity string \`json:"shipping_city" validate:"required"\` ShippingState string \`json:"shipping_state" validate:"required"\` ShippingZip string \`json:"shipping_zip" validate:"required"\` ShippingCountry string \`json:"shipping_country" validate:"required"\` PaymentMethod string \`json:"payment_method,omitempty"\` Notes string \`json:"notes,omitempty"\` } type UpdateOrderStatusRequest struct { Status OrderStatus \`json:"status" validate:"required,oneof=pending processing shipped delivered cancelled refunded"\` } `, // Handlers 'handlers/handler.go': `package handlers import ( "encoding/json" "fmt" "net/http" "reflect" "{{projectName}}/config" "github.com/go-chi/render" "github.com/go-playground/validator/v10" "github.com/rs/zerolog/log" "gorm.io/gorm" ) type Handler struct { db *gorm.DB cfg *config.Config validate *validator.Validate } func NewHandler(db *gorm.DB, cfg *config.Config) *Handler { v := validator.New() // Register custom validators v.RegisterValidation("password", validatePassword) return &Handler{ db: db, cfg: cfg, validate: v, } } // Custom validation functions func validatePassword(fl validator.FieldLevel) bool { password := fl.Field().String() // At least 6 characters, one uppercase, one lowercase, one digit if len(password) < 6 { return false } var hasUpper, hasLower, hasDigit bool for _, char := range password { switch { case 'A' <= char && char <= 'Z': hasUpper = true case 'a' <= char && char <= 'z': hasLower = true case '0' <= char && char <= '9': hasDigit = true } } return hasUpper && hasLower && hasDigit } // Response helpers type ErrorResponse struct { Error string \`json:"error"\` Details []ValidationError \`json:"details,omitempty"\` } type ValidationError struct { Field string \`json:"field"\` Message string \`json:"message"\` } func (h *Handler) respondWithError(w http.ResponseWriter, code int, message string) { render.Status(r, code) render.JSON(w, r, ErrorResponse{Error: message}) } func (h *Handler) respondWithValidationError(w http.ResponseWriter, r *http.Request, err error) { var details []ValidationError if validationErrors, ok := err.(validator.ValidationErrors); ok { for _, e := range validationErrors { details = append(details, ValidationError{ Field: e.Field(), Message: h.getErrorMessage(e), }) } } render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrorResponse{ Error: "Validation failed", Details: details, }) } func (h *Handler) getErrorMessage(e validator.FieldError) string { switch e.Tag() { case "required": return "This field is required" case "email": return "Invalid email format" case "min": if e.Type().Kind() == reflect.String { return fmt.Sprintf("Must be at least %s characters", e.Param()) } return fmt.Sprintf("Must be at least %s", e.Param()) case "max": if e.Type().Kind() == reflect.String { return fmt.Sprintf("Must be at most %s characters", e.Param()) } return fmt.Sprintf("Must be at most %s", e.Param()) case "oneof": return fmt.Sprintf("Must be one of: %s", e.Param()) case "e164": return "Invalid phone number format (use E.164 format)" case "password": return "Password must contain uppercase, lowercase, and digit" case "gtefield": return fmt.Sprintf("Must be greater than or equal to %s", e.Param()) default: return "Invalid value" } } // Bind and validate request func (h *Handler) bindAndValidate(r *http.Request, v interface{}) error { if err := json.NewDecoder(r.Body).Decode(v); err != nil { return fmt.Errorf("invalid request format: %w", err) } if err := h.validate.Struct(v); err != nil { return err } return nil } // Pagination helpers type PaginatedResponse struct { Data interface{} \`json:"data"\` Total int64 \`json:"total"\` Page int \`json:"page"\` Limit int \`json:"limit"\` TotalPages int \`json:"total_pages"\` HasNext bool \`json:"has_next"\` HasPrev bool \`json:"has_prev"\` } func (h *Handler) paginate(query *gorm.DB, page, limit int, result interface{}) (*PaginatedResponse, error) { var total int64 // Count total records countQuery := *query if err := countQuery.Count(&total).Error; err != nil { return nil, err } // Calculate offset offset := (page - 1) * limit // Fetch paginated results if err := query.Offset(offset).Limit(limit).Find(result).Error; err != nil { return nil, err } // Calculate total pages totalPages := int(total) / limit if int(total)%limit > 0 { totalPages++ } return &PaginatedResponse{ Data: result, Total: total, Page: page, Limit: limit, TotalPages: totalPages, HasNext: page < totalPages, HasPrev: page > 1, }, nil } `, 'handlers/auth.go': `package handlers import ( "net/http" "time" "{{projectName}}/models" "{{projectName}}/utils" "github.com/go-chi/render" "github.com/rs/zerolog/log" ) // @Summary Register a new user // @Description Create a new user account // @Tags auth // @Accept json // @Produce json // @Param user body models.RegisterRequest true "User registration data" // @Success 201 {object} models.UserResponse // @Failure 400 {object} handlers.ErrorResponse // @Failure 409 {object} handlers.ErrorResponse // @Router /auth/register [post] func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { var req models.RegisterRequest if err := h.bindAndValidate(r, &req); err != nil { if _, ok := err.(validator.ValidationErrors); ok { h.respondWithValidationError(w, r, err) return } h.respondWithError(w, http.StatusBadRequest, "Invalid request format") return } // Check if user exists var existingUser models.User if err := h.db.Where("email = ?", req.Email).First(&existingUser).Error; err == nil { h.respondWithError(w, http.StatusConflict, "Email already registered") return } // Create new user user := models.User{ Email: req.Email, FirstName: req.FirstName, LastName: req.LastName, Phone: req.Phone, } if err := user.SetPassword(req.Password); err != nil { log.Error().Err(err).Msg("Failed to hash password") h.respondWithError(w, http.StatusInternalServerError, "Failed to create user") return } if err := h.db.Create(&user).Error; err != nil { log.Error().Err(err).Msg("Failed to create user") h.respondWithError(w, http.StatusInternalServerError, "Failed to create user") return } log.Info().Uint("user_id", user.ID).Str("email", user.Email).Msg("User registered") render.Status(r, http.StatusCreated) render.JSON(w, r, user.ToResponse()) } // @Summary Login user // @Description Authenticate user and return JWT token // @Tags auth // @Accept json // @Produce json // @Param credentials body models.LoginRequest true "Login credentials" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} handlers.ErrorResponse // @Failure 401 {object} handlers.ErrorResponse // @Router /auth/login [post] func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { var req models.LoginRequest if err := h.bindAndValidate(r, &req); err != nil { if _, ok := err.(validator.ValidationErrors); ok { h.respondWithValidationError(w, r, err) return } h.respondWithError(w, http.StatusBadRequest, "Invalid request format") return } // Find user var user models.User if err := h.db.Where("email = ?", req.Email).First(&user).Error; err != nil { h.respondWithError(w, http.StatusUnauthorized, "Invalid credentials") return } // Check password if !user.CheckPassword(req.Password) { h.respondWithError(w, http.StatusUnauthorized, "Invalid credentials") return } // Check if user is active if !user.Active { h.respondWithError(w, http.StatusUnauthorized, "Account is disabled") return } // Generate JWT token token, err := utils.GenerateJWT(user.ID, user.Email, user.Role, h.cfg.JWTSecret, h.cfg.JWTExpirationHours) if err != nil { log.Error().Err(err).Msg("Failed to generate token") h.respondWithError(w, http.StatusInternalServerError, "Failed to generate token") return } log.Info().Uint("user_id", user.ID).Str("email", user.Email).Msg("User logged in") render.JSON(w, r, map[string]interface{}{ "token": token, "user": user.ToResponse(), "expires_in": h.cfg.JWTExpirationHours * 3600, // in seconds }) } // @Summary Get current user // @Description Get the currently authenticated user // @Tags auth // @Accept json // @Produce json // @Security Bearer // @Success 200 {object} models.UserResponse // @Failure 401 {object} handlers.ErrorResponse // @Router /auth/me [get] func (h *Handler) GetCurrentUser(w http.ResponseWriter, r *http.Request) { userID := r.Context().Value("userID").(uint) var user models.User if err := h.db.First(&user, userID).Error; err != nil { h.respondWithError(w, http.StatusNotFound, "User not found") return } render.JSON(w, r, user.ToResponse()) } // @Summary Change password // @Description Change the current user's password // @Tags auth // @Accept json // @Produce json // @Security Bearer // @Param request body models.ChangePasswordRequest true "Password change data" // @Success 200 {object} map[string]string // @Failure 400 {object} handlers.ErrorResponse // @Failure 401 {object} handlers.ErrorResponse // @Router /auth/change-password [post] func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) { userID := r.Context().Value("userID").(uint) var req models.ChangePasswordRequest if err := h.bindAndValidate(r, &req); err != nil { if _, ok := err.(validator.ValidationErrors); ok { h.respondWithValidationError(w, r, err) return } h.respondWithError(w, http.StatusBadRequest, "Invalid request format") return } var user models.User if err := h.db.First(&user, userID).Error; err != nil { h.respondWithError(w, http.StatusNotFound, "User not found") return } // Verify current password if !user.CheckPassword(req.CurrentPassword) { h.respondWithError(w, http.StatusUnauthorized, "Current password is incorrect") return } // Set new password if err := user.SetPassword(req.NewPassword); err != nil { log.Error().Err(err).Msg("Failed to hash password") h.respondWithError(w, http.StatusInternalServerError, "Failed to update password") return } if err := h.db.Save(&user).Error; err != nil { log.Error().Err(err).Msg("Failed to update user") h.respondWithError(w, http.StatusInternalServerError, "Failed to update password") return } log.Info().Uint("user_id", user.ID).Msg("Password changed") render.JSON(w, r, map[string]string{ "message": "Password changed successfully", }) } `, 'handlers/user.go': `package handlers import ( "net/http" "strconv" "{{projectName}}/models" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/rs/zerolog/log" ) // @Summary List all users // @Description Get a list of all users (admin only) // @Tags users // @Accept json // @Produce json // @Security Bearer // @Param page query int false "Page number" default(1) // @Param limit query int false "Items per page" default(10) // @Success 200 {object} handlers.PaginatedResponse // @Failure 401 {object} handlers.ErrorResponse // @Failure 403 {object} handlers.ErrorResponse // @Router /users [get] func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) { page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit < 1 || limit > 100 { limit = 10 } var users []models.User query := h.db.Model(&models.User{}) response, err := h.paginate(query, page, limit, &users) if err != nil { log.Error().Err(err).Msg("Failed to fetch users") h.respondWithError(w, http.StatusInternalServerError, "Failed to fetch users") return } // Convert to response format userResponses := make([]models.UserResponse, len(users)) for i, user := range users { userResponses[i] = user.ToResponse() } response.Data = userResponses render.JSON(w, r, response) } // @Summary Get user by ID // @Description Get a specific user by ID // @Tags users // @Accept json // @Produce json // @Security Bearer // @Param id path int true "User ID" // @Success 200 {object} models.UserResponse // @Failure 400 {object} handlers.ErrorResponse // @Failure 401 {object} handlers.ErrorResponse // @Failure 404 {object} handlers.ErrorResponse // @Router /users/{id} [get] func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.ParseUint(idParam, 10, 32) if err != nil { h.respondWithError(w, http.StatusBadRequest, "Invalid user ID") return } var user models.User if err := h.db.First(&user, id).Error; err != nil { h.respondWithError(w, http.StatusNotFound, "User not found") return } render.JSON(w, r, user.ToResponse()) } // @Summary Update user // @Description Update a user's information // @Tags users // @Accept json // @Produce json // @Security Bearer // @Param id path int true "User ID" // @Param user body models.UpdateUserRequest true "User update data" // @Success 200 {object} models.UserResponse // @Failure 400 {object} handlers.ErrorResponse // @Failure 401 {object} handlers.ErrorResponse // @Failure 404 {object} handlers.ErrorResponse // @Router /users/{id} [put] func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) { // Get user ID from URL idParam := chi.URLParam(r, "id") id, err := strconv.ParseUint(idParam, 10, 32) if err != nil { h.respondWithError(w, http.StatusBadRequest, "Invalid user ID") return } // Check if user can update this profile currentUserID := r.Context().Value("userID").(uint) currentUserRole := r.Context().Value("userRole").(string) if currentUserID != uint(id) && currentUserRole != "admin" { h.respondWithError(w, http.StatusForbidden, "Cannot update other users") return } var user models.User if err := h.db.First(&user, id).Error; err != nil { h.respondWithError(w, http.StatusNotFound, "User not found") return } var req models.UpdateUserRequest if err := h.bindAndValidate(r, &req); err != nil { if _, ok := err.(validator.ValidationErrors); ok { h.respondWithValidationError(w, r, err) return } h.respondWithError(w, http.StatusBadRequest, "Invalid request format") return } // Update fields if req.FirstName != nil { user.FirstName = *req.FirstName } if req.LastName != nil { user.LastName = *req.LastName } if req.Phone != nil { user.Phone = *req.Phone } if err := h.db.Save(&user).Error; err != nil { log.Error().Err(err).Msg("Failed to update user") h.respondWithError(w, http.StatusInternalServerError, "Failed to update user") return } log.Info().Uint("user_id", user.ID).Msg("User updated") render.JSON(w, r, user.ToResponse()) } // @Summary Delete user // @Description Delete a user (admin only) // @Tags users // @Accept json // @Produce json // @Security Bearer // @Param id path int true "User ID" // @Success 204 // @Failure 400 {object} handlers.ErrorResponse // @Failure 401 {object} handlers.ErrorResponse // @Failure 403 {object} handlers.ErrorResponse // @Failure 404 {object} handlers.ErrorResponse // @Router /users/{id} [delete] func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.ParseUint(idParam, 10, 32) if err != nil { h.respondWithError(w, http.StatusBadRequest, "Invalid user ID") return } // Prevent self-deletion currentUserID := r.Context().Value("userID").(uint) if currentUserID == uint(id) { h.respondWithError(w, http.StatusBadRequest, "Cannot delete yourself") return } result := h.db.Delete(&models.User{}, id) if result.Error != nil { log.Error().Err(result.Error).Msg("Failed to delete user") h.respondWithError(w, http.StatusInternalServerError, "Failed to delete user") return } if result.RowsAffected == 0 { h.respondWithError(w, http.StatusNotFound, "User not found") return } log.Info().Uint64("user_id", id).Msg("User deleted") w.WriteHeader(http.StatusNoContent) } `, 'handlers/product.go': `package handlers import ( "net/http" "strconv" "{{projectName}}/models" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/rs/zerolog/log" "gorm.io/gorm" ) // @Summary List all products // @Description Get a list of all products with pagination and filtering // @Tags products // @Accept json // @Produce json // @Param page query int false "Page number" default(1) // @Param limit query int false "Items per page" default(10) // @Param search query string false "Search in name and description" // @Param category query string false "Filter by category" // @Param brand query string false "Filter by brand" // @Param min_price query number false "Minimum price" // @Param max_price query number false "Maximum price" // @Param in_stock query bool false "Only show in-stock items" // @Param featured query bool false "Only show featured items" // @Param sort_by query string false "Sort by field" Enums(name, price, created_at, stock) // @Param sort_order query string false "Sort order" Enums(asc, desc) // @Success 200 {object} handlers.PaginatedResponse // @Router /products [get] func (h *Handler) ListProducts(w http.ResponseWriter, r *http.Request) { var req models.ProductListRequest // Parse query parameters req.Page, _ = strconv.Atoi(r.URL.Query().Get("page")) if req.Page < 1 { req.Page = 1 } req.Limit, _ = strconv.Atoi(r.URL.Query().Get("limit")) if req.Limit < 1 || req.Limit > 100 { req.Limit = 10 } req.Search = r.URL.Query().Get("search") req.Category = r.URL.Query().Get("category") req.Brand = r.URL.Query().Get("brand") req.MinPrice, _ = strconv.ParseFloat(r.URL.Query().Get("min_price"), 64) req.MaxPrice, _ = strconv.ParseFloat(r.URL.Query().Get("max_price"), 64) req.InStock, _ = strconv.ParseBool(r.URL.Query().Get("in_stock")) req.Featured, _ = strconv.ParseBool(r.URL.Query().Get("featured")) req.SortBy = r.URL.Query().Get("sort_by") req.SortOrder = r.URL.Query().Get("sort_order") // Set defaults if req.SortBy == "" { req.SortBy = "created_at" } if req.SortOrder == "" { req.SortOrder = "desc" } // Validate request if err := h.validate.Struct(&req); err != nil { h.respondWithValidationError(w, r, err) return } // Build query query := h.db.Model(&models.Product{}).Where("active = ?", true) // Apply filters if req.Search != "" { search := "%" + req.Search + "%" query = query.Where("name ILIKE ? OR description ILIKE ?", search, search) } if req.Category != "" { query = query.Where("category = ?", req.Category) } if req.Brand != "" { query = query.Where("brand = ?", req.Brand) } if req.MinPrice > 0 { query = query.Where("price >= ?", req.MinPrice) } if req.MaxPrice > 0 { query = query.Where("price <= ?", req.MaxPrice) } if req.InStock { query = query.Where("stock > 0") } if req.Featured { query = query.Where("featured = ?", true) } // Apply sorting orderClause := req.SortBy + " " + req.SortOrder query = query.Order(orderClause) // Execute pagination var products []models.Product response, err := h.paginate(query, req.Page, req.Limit, &products) if err != nil { log.Error().Err(err).Msg("Failed to fetch products") h.respondWithError(w, http.StatusInternalServerError, "Failed to fetch products") return } response.Data = products render.JSON(w, r, response) } // @Summary Get product by ID // @Description Get a specific product by ID // @Tags products // @Accept json // @Produce json // @Param id path int true "Product ID" // @Success 200 {object} models.Product // @Failure 400 {object} handlers.ErrorResponse // @Failure 404 {object} handlers.ErrorResponse // @Router /products/{id} [get] func (h *Handler) GetProduct(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.ParseUint(idParam, 10, 32) if err != nil { h.respondWithError(w, http.StatusBadRequest, "Invalid product ID") return } var product models.Product if err := h.db.First(&product, id).Error; err != nil { if err == gorm.ErrRecordNotFound { h.respondWithError(w, http.StatusNotFound, "Product not found") return } log.Error().Err(err).Msg("Failed to fetch product") h.respondWithError(w, http.StatusInternalServerError, "Failed to fetch product") return } render.JSON(w, r, product) } // @Summary Create a new product // @Description Create a new product (admin only) // @Tags products // @Accept json // @Produce json // @Security Bearer // @Param product body models.CreateProductRequest true "Product data" // @Success 201 {object} models.Product // @Failure 400 {object} handlers.ErrorResponse // @Failure 401 {object} handlers.ErrorResponse // @Failure 409 {object} handlers.ErrorResponse // @Router /products [post] func (h *Handler) CreateProduct(w http.ResponseWriter, r *http.Request) { var req models.CreateProductRequest if err := h.bindAndValidate(r, &req); err != nil { if _, ok := err.(validator.ValidationErrors); ok { h.respondWithValidationError(w, r, err) return } h.respondWithError(w, http.StatusBadRequest, "Invalid request format") return } // Check if SKU exists var existingProduct models.Product if err := h.db.Where("sku = ?", req.SKU).First(&existingProduct).Error; err == nil { h.respondWithError(w, http.StatusConflict, "Product with this SKU already exists") return } // Check if barcode exists if req.Barcode != "" { if err := h.db.Where("barcode = ?", req.Barcode).First(&existingProduct).Error; err == nil { h.respondWithError(w, http.StatusConflict, "Product with this barcode already exists") return } } product := models.Product{ Name: req.Name, Description: req.Description, Price: req.Price, Cost: req.Cost, Stock: req.Stock, SKU: req.SKU, Barcode: req.Barcode, Category: req.Category, Brand: req.Brand, Weight: req.Weight, Dimensions: req.Dimensions, Active: true, } if err := h.db.Create(&product).Error; err != nil { log.Error().Err(err).Msg("Failed to create product") h.respondWithError(w, http.StatusInternalServerError, "Failed to create product") return } log.Info().Uint("product_id", product.ID).Str("sku", product.SKU).Msg("Product created") render.Status(r, http.StatusCreated) render.JSON(w, r, product) } // @Summary Update product // @Description Update a product (admin only) // @Tags products // @Accept json // @Produce json // @Security Bearer // @Param id path int true "Product ID" // @Param product body models.UpdateProductRequest true "Product update data" // @Success 200 {object} models.Product // @Failure 400 {object} handlers.ErrorResponse // @Failure 401 {object} handlers.ErrorResponse // @Failure 404 {object} handlers.ErrorResponse // @Router /products/{id} [put] func (h *Handler) UpdateProduct(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.ParseUint(idParam, 10, 32) if err != nil { h.respondWithError(w, http.StatusBadRequest, "Invalid product ID") return } var product models.Product if err := h.db.First(&product, id).Error; err != nil { if err == gorm.ErrRecordNotFound { h.respondWithError(w, http.StatusNotFound, "Product not found") return } log.Error().Err(err).Msg("Failed to fetch product") h.respondWithError(w, http.StatusInternalServerError, "Failed to fetch product") return } var req models.UpdateProductRequest if err := h.bindAndValidate(r, &req); err != nil { if _, ok := err.(validator.ValidationErrors); ok { h.respondWithValidationError(w, r, err) return } h.respondWithError(w, http.StatusBadRequest, "Invalid request format") return } // Update fields if req.Name != nil { product.Name = *req.Name } if req.Description != nil { product.Description = *req.Description } if req.Price != nil { product.Price = *req.Price } if req.Cost != nil { product.Cost = *req.Cost } if req.Stock != nil { product.Stock = *req.Stock } if req.Category != nil { product.Category = *req.Category } if req.Brand != nil { product.Brand = *req.Brand } if req.Weight != nil { product.Weight = *req.Weight } if req.Dimensions != nil { product.Dimensions = *req.Dimensions } if req.Active != nil { product.Active = *req.Active } if req.Featured != nil { product.Featured = *req.Featured } if err := h.db.Save(&product).Error; err != nil { log.Error().Err(err).Msg("Failed to update product") h.respondWithError(w, http.StatusInternalServerError, "Failed to update product") return } log.Info().Uint("product_id", product.ID).Msg("Product updated") render.JSON(w, r, product) } // @Summary Delete product // @Description Delete a product (admin only) // @Tags products // @Accept json // @Produce json // @Security Bearer // @Param id path int true "Product ID" // @Success 204 // @Failure 400 {object} handlers.ErrorResponse // @Failure 401 {object} handlers.ErrorResponse // @Failure 404 {object} handlers.ErrorResponse // @Router /products/{id} [delete] func (h *Handler) DeleteProduct(w http.ResponseWriter, r *http.Request) { idParam := chi.URLParam(r, "id") id, err := strconv.ParseUint(idParam, 10, 32) if err != nil { h.respondWithError(w, http.StatusBadRequest, "Invalid product ID") return } result := h.db.Delete(&models.Product{}, id) if result.Error != nil { log.Error().Err(result.Error).Msg("Failed to delete product") h.respondWithError(w, http.StatusInternalServerError, "Failed to delete product") return } if result.RowsAffected == 0 { h.respondWithError(w, http.StatusNotFound, "Product not found") return } log.Info().Uint64("product_id", id).Msg("Product deleted") w.WriteHeader(http.StatusNoContent) } `, // Routes 'routes/routes.go': `package routes import ( "{{projectName}}/config" "{{projectName}}/handlers" "{{projectName}}/middleware" "github.com/go-chi/chi/v5" ) func RegisterRoutes(r chi.Router, h *handlers.Handler, cfg *config.Config) { // Public routes r.Group(func(r chi.Router) { // Auth routes r.Route("/auth", func(r chi.Router) { r.Post("/register", h.Register) r.Post("/login", h.Login) }) // Public product routes r.Route("/products", func(r chi.Router) { r.Get("/", h.ListProducts) r.Get("/{id}", h.GetProduct) }) }) // Protected routes r.Group(func(r chi.Router) { r.Use(middleware.JWT(cfg)) // Auth routes r.Route("/auth", func(r chi.Router) { r.Get("/me", h.GetCurrentUser) r.Post("/change-password", h.ChangePassword) }) // User routes r.Route("/users", func(r chi.Router) { r.With(middleware.RequireRole("admin")).Get("/", h.ListUsers) r.Get("/{id}", h.GetUser) r.Put("/{id}", h.UpdateUser) r.With(middleware.RequireRole("admin")).Delete("/{id}", h.DeleteUser) }) // Product management routes r.Route("/products", func(r chi.Router) { r.With(middleware.RequireRole("admin", "manager")).Post("/", h.CreateProduct) r.With(middleware.RequireRole("admin", "manager")).Put("/{id}", h.UpdateProduct) r.With(middleware.RequireRole("admin")).Delete("/{id}", h.DeleteProduct) }) // Order routes r.Route("/orders", func