@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,758 lines (1,518 loc) • 63.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.fiberTemplate = void 0;
exports.fiberTemplate = {
id: 'fiber',
name: 'fiber',
displayName: 'Fiber Framework',
description: 'Express-inspired web framework written in Go with zero memory allocation and performance',
language: 'go',
framework: 'fiber',
version: '2.52.0',
tags: ['go', 'fiber', 'api', 'rest', 'express-like', 'performance', 'zero-allocation'],
port: 3000,
dependencies: {},
features: ['authentication', 'validation', 'logging', 'cors', 'documentation', 'websockets', 'session-management'],
files: {
// Go module configuration
'go.mod': `module {{projectName}}
go 1.21
require (
github.com/gofiber/fiber/v2 v2.52.0
github.com/gofiber/jwt/v4 v4.0.0
github.com/gofiber/swagger v1.0.0
github.com/gofiber/websocket/v2 v2.2.1
github.com/gofiber/contrib/fiberzap/v2 v2.0.0
github.com/gofiber/storage/redis/v3 v3.1.0
github.com/joho/godotenv v1.5.1
github.com/go-playground/validator/v10 v10.16.0
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/swaggo/swag v1.16.2
go.uber.org/zap v1.26.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
)
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/andybalholm/brotli v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fasthttp/websocket v1.5.7 // 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/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/klauspost/compress v1.17.4 // indirect
github.com/leodido/go-urn v1.2.4 // 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-runewidth v0.0.15 // indirect
github.com/mattn/go-sqlite3 v1.14.19 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/tinylib/msgp v1.1.9 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/multierr v1.11.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 (
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"{{projectName}}/config"
"{{projectName}}/database"
_ "{{projectName}}/docs" // swagger docs
"{{projectName}}/handlers"
"{{projectName}}/middleware"
"{{projectName}}/routes"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/helmet"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/gofiber/swagger"
"github.com/gofiber/contrib/fiberzap/v2"
"github.com/gofiber/websocket/v2"
"github.com/joho/godotenv"
"go.uber.org/zap"
)
// @title {{projectName}} API
// @version 1.0
// @description API server for {{projectName}} built with Fiber framework
// @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:3000
// @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.Println("No .env file found")
}
// Initialize configuration
cfg := config.New()
// Initialize logger
logger, err := zap.NewProduction()
if err != nil {
panic(err)
}
if cfg.Environment == "development" {
logger, _ = zap.NewDevelopment()
}
defer logger.Sync()
// Initialize database
db, err := database.Initialize(cfg)
if err != nil {
logger.Fatal("Failed to initialize database", zap.Error(err))
}
// Run migrations
if err := database.Migrate(db); err != nil {
logger.Fatal("Failed to run migrations", zap.Error(err))
}
// Create Fiber app with configuration
app := fiber.New(fiber.Config{
AppName: "{{projectName}}",
ServerHeader: "Fiber",
DisableStartupMessage: false,
Prefork: cfg.Prefork,
StrictRouting: true,
CaseSensitive: true,
BodyLimit: 4 * 1024 * 1024, // 4MB
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
ErrorHandler: middleware.CustomErrorHandler,
})
// Global middleware
app.Use(recover.New())
app.Use(requestid.New())
app.Use(fiberzap.New(fiberzap.Config{
Logger: logger,
Fields: []string{"requestId", "latency", "status", "method", "url", "ip"},
}))
app.Use(helmet.New())
app.Use(compress.New(compress.Config{
Level: compress.LevelBestSpeed,
}))
app.Use(cors.New(cors.Config{
AllowOrigins: cfg.AllowedOrigins,
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS,PATCH",
AllowHeaders: "Origin,Content-Type,Accept,Authorization,X-Request-ID",
ExposeHeaders: "X-Request-ID",
AllowCredentials: true,
MaxAge: 86400,
}))
app.Use(middleware.RateLimiter(cfg))
// Health check
app.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "healthy",
"time": time.Now().UTC(),
})
})
// Swagger documentation
app.Get("/swagger/*", swagger.New())
// WebSocket upgrade middleware
app.Use("/api/v1/ws", func(c *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(c) {
return c.Next()
}
return fiber.ErrUpgradeRequired
})
// Initialize handlers
h := handlers.NewHandler(db, cfg, logger)
// API routes
api := app.Group("/api/v1")
routes.SetupRoutes(api, h, cfg)
// Graceful shutdown
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
logger.Info("Shutting down server...")
if err := app.Shutdown(); err != nil {
logger.Error("Server shutdown error", zap.Error(err))
}
}()
// Start server
addr := fmt.Sprintf(":%d", cfg.Port)
logger.Info("Starting server", zap.String("address", addr))
if err := app.Listen(addr); err != nil {
logger.Fatal("Failed to start server", zap.Error(err))
}
}
`,
// Configuration
'config/config.go': `package config
import (
"os"
"strconv"
"strings"
"time"
)
type Config struct {
Environment string
Port int
Prefork bool
// Database
DBHost string
DBPort int
DBUser string
DBPassword string
DBName string
DBSSLMode string
// JWT
JWTSecret string
JWTAccessExpiration time.Duration
JWTRefreshExpiration time.Duration
// Redis
RedisAddr string
RedisPassword string
RedisDB int
// Rate limiting
RateLimitRequests int
RateLimitDuration time.Duration
// CORS
AllowedOrigins string
// Session
SessionSecret string
SessionExpiration time.Duration
}
func New() *Config {
return &Config{
Environment: getEnv("ENVIRONMENT", "development"),
Port: getEnvAsInt("PORT", 3000),
Prefork: getEnvAsBool("PREFORK", false),
// 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"),
JWTAccessExpiration: time.Duration(getEnvAsInt("JWT_ACCESS_EXPIRATION_MINUTES", 15)) * time.Minute,
JWTRefreshExpiration: time.Duration(getEnvAsInt("JWT_REFRESH_EXPIRATION_DAYS", 7)) * 24 * time.Hour,
// 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: getEnv("ALLOWED_ORIGINS", "*"),
// Session
SessionSecret: getEnv("SESSION_SECRET", "session-secret-change-this"),
SessionExpiration: time.Duration(getEnvAsInt("SESSION_EXPIRATION_HOURS", 24)) * time.Hour,
}
}
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"
"{{projectName}}/config"
"{{projectName}}/models"
"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{
PrepareStmt: true,
QueryFields: 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)
return db, nil
}
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(
&models.User{},
&models.Product{},
&models.RefreshToken{},
&models.Session{},
)
}
`,
// 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:"-"\`
Name string \`gorm:"not null" json:"name" validate:"required,min=2,max=100"\`
Avatar string \`json:"avatar,omitempty"\`
Bio string \`json:"bio,omitempty" validate:"max=500"\`
Role string \`gorm:"default:user" json:"role" validate:"omitempty,oneof=user admin moderator"\`
Active bool \`gorm:"default:true" json:"active"\`
Verified bool \`gorm:"default:false" json:"verified"\`
RefreshTokens []RefreshToken \`gorm:"foreignKey:UserID" json:"-"\`
Sessions []Session \`gorm:"foreignKey:UserID" json:"-"\`
}
type RefreshToken struct {
ID uint \`gorm:"primarykey" json:"id"\`
Token string \`gorm:"uniqueIndex;not null" json:"token"\`
UserID uint \`gorm:"not null" json:"user_id"\`
ExpiresAt time.Time \`gorm:"not null" json:"expires_at"\`
CreatedAt time.Time \`json:"created_at"\`
}
type Session struct {
ID string \`gorm:"primarykey" json:"id"\`
UserID uint \`gorm:"not null;index" json:"user_id"\`
Data string \`json:"data"\`
ExpiresAt time.Time \`gorm:"not null;index" json:"expires_at"\`
CreatedAt time.Time \`json:"created_at"\`
UpdatedAt time.Time \`json:"updated_at"\`
}
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
}
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"\`
Name string \`json:"name" validate:"required,min=2,max=100"\`
}
type UpdateProfileRequest struct {
Name *string \`json:"name,omitempty" validate:"omitempty,min=2,max=100"\`
Avatar *string \`json:"avatar,omitempty" validate:"omitempty,url"\`
Bio *string \`json:"bio,omitempty" validate:"omitempty,max=500"\`
}
type ChangePasswordRequest struct {
CurrentPassword string \`json:"current_password" validate:"required"\`
NewPassword string \`json:"new_password" validate:"required,min=6,max=72"\`
}
type RefreshTokenRequest struct {
RefreshToken string \`json:"refresh_token" validate:"required"\`
}
type TokenResponse struct {
AccessToken string \`json:"access_token"\`
RefreshToken string \`json:"refresh_token"\`
TokenType string \`json:"token_type"\`
ExpiresIn int \`json:"expires_in"\`
}
type UserResponse struct {
ID uint \`json:"id"\`
Email string \`json:"email"\`
Name string \`json:"name"\`
Avatar string \`json:"avatar,omitempty"\`
Bio string \`json:"bio,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,
Name: u.Name,
Avatar: u.Avatar,
Bio: u.Bio,
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" json:"price" validate:"required,min=0"\`
Stock int \`gorm:"default:0" json:"stock" validate:"min=0"\`
SKU string \`gorm:"uniqueIndex" json:"sku" validate:"required,min=3,max=50"\`
Category string \`gorm:"index" json:"category" validate:"required,min=1,max=100"\`
Tags []string \`gorm:"type:text[]" json:"tags"\`
Images []string \`gorm:"type:text[]" json:"images"\`
Active bool \`gorm:"default:true;index" json:"active"\`
Featured bool \`gorm:"default:false;index" json:"featured"\`
}
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"\`
Stock int \`json:"stock" validate:"min=0"\`
SKU string \`json:"sku" validate:"required,min=3,max=50"\`
Category string \`json:"category" validate:"required,min=1,max=100"\`
Tags []string \`json:"tags" validate:"max=10,dive,min=1,max=50"\`
Images []string \`json:"images" validate:"max=10,dive,url"\`
}
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"\`
Stock *int \`json:"stock,omitempty" validate:"omitempty,min=0"\`
Category *string \`json:"category,omitempty" validate:"omitempty,min=1,max=100"\`
Tags *[]string \`json:"tags,omitempty" validate:"omitempty,max=10,dive,min=1,max=50"\`
Images *[]string \`json:"images,omitempty" validate:"omitempty,max=10,dive,url"\`
Active *bool \`json:"active,omitempty"\`
Featured *bool \`json:"featured,omitempty"\`
}
type ProductListRequest struct {
Page int \`query:"page" validate:"min=1"\`
Limit int \`query:"limit" validate:"min=1,max=100"\`
Category string \`query:"category" validate:"omitempty,min=1,max=100"\`
Tags []string \`query:"tags" validate:"omitempty,max=5,dive,min=1,max=50"\`
Search string \`query:"search" validate:"omitempty,min=1,max=100"\`
MinPrice float64 \`query:"min_price" validate:"omitempty,min=0"\`
MaxPrice float64 \`query:"max_price" validate:"omitempty,min=0,gtefield=MinPrice"\`
Featured bool \`query:"featured"\`
SortBy string \`query:"sort_by" validate:"omitempty,oneof=name price created_at stock"\`
Order string \`query:"order" validate:"omitempty,oneof=asc desc"\`
}
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"\`
}
`,
// Handlers
'handlers/handler.go': `package handlers
import (
"{{projectName}}/config"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
"gorm.io/gorm"
)
type Handler struct {
db *gorm.DB
cfg *config.Config
logger *zap.Logger
validate *validator.Validate
}
func NewHandler(db *gorm.DB, cfg *config.Config, logger *zap.Logger) *Handler {
v := validator.New()
// Register custom validators
v.RegisterValidation("password", validatePassword)
return &Handler{
db: db,
cfg: cfg,
logger: logger,
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
}
// Validation error response
type ValidationErrorResponse struct {
Error string \`json:"error"\`
Fields []ValidationError \`json:"fields,omitempty"\`
}
type ValidationError struct {
Field string \`json:"field"\`
Message string \`json:"message"\`
}
func (h *Handler) formatValidationErrors(err error) ValidationErrorResponse {
var errors []ValidationError
if validationErrors, ok := err.(validator.ValidationErrors); ok {
for _, e := range validationErrors {
errors = append(errors, ValidationError{
Field: e.Field(),
Message: h.getErrorMessage(e),
})
}
}
return ValidationErrorResponse{
Error: "Validation failed",
Fields: errors,
}
}
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 "url":
return "Invalid URL 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"
}
}
// Common error responses
func (h *Handler) unauthorizedError() fiber.Map {
return fiber.Map{
"error": "Unauthorized",
}
}
func (h *Handler) forbiddenError() fiber.Map {
return fiber.Map{
"error": "Forbidden",
}
}
func (h *Handler) notFoundError(resource string) fiber.Map {
return fiber.Map{
"error": fmt.Sprintf("%s not found", resource),
}
}
func (h *Handler) internalError() fiber.Map {
return fiber.Map{
"error": "Internal server error",
}
}
`,
'handlers/auth.go': `package handlers
import (
"crypto/rand"
"encoding/base64"
"time"
"{{projectName}}/models"
"{{projectName}}/utils"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
)
// @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.ValidationErrorResponse
// @Failure 409 {object} map[string]string
// @Router /auth/register [post]
func (h *Handler) Register(c *fiber.Ctx) error {
var req models.RegisterRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request format",
})
}
if err := h.validate.Struct(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(h.formatValidationErrors(err))
}
// Check if user exists
var existingUser models.User
if err := h.db.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
return c.Status(fiber.StatusConflict).JSON(fiber.Map{
"error": "Email already registered",
})
}
// Create new user
user := models.User{
Email: req.Email,
Name: req.Name,
}
if err := user.SetPassword(req.Password); err != nil {
h.logger.Error("Failed to hash password", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
if err := h.db.Create(&user).Error; err != nil {
h.logger.Error("Failed to create user", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
h.logger.Info("User registered", zap.Uint("user_id", user.ID), zap.String("email", user.Email))
return c.Status(fiber.StatusCreated).JSON(user.ToResponse())
}
// @Summary Login user
// @Description Authenticate user and return JWT tokens
// @Tags auth
// @Accept json
// @Produce json
// @Param credentials body models.LoginRequest true "Login credentials"
// @Success 200 {object} models.TokenResponse
// @Failure 400 {object} handlers.ValidationErrorResponse
// @Failure 401 {object} map[string]string
// @Router /auth/login [post]
func (h *Handler) Login(c *fiber.Ctx) error {
var req models.LoginRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request format",
})
}
if err := h.validate.Struct(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(h.formatValidationErrors(err))
}
// Find user
var user models.User
if err := h.db.Where("email = ?", req.Email).First(&user).Error; err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid credentials",
})
}
// Check password
if !user.CheckPassword(req.Password) {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid credentials",
})
}
// Check if user is active
if !user.Active {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Account is disabled",
})
}
// Generate tokens
accessToken, err := utils.GenerateAccessToken(user.ID, user.Email, user.Role, h.cfg.JWTSecret, h.cfg.JWTAccessExpiration)
if err != nil {
h.logger.Error("Failed to generate access token", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
refreshToken, err := h.generateRefreshToken()
if err != nil {
h.logger.Error("Failed to generate refresh token", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
// Save refresh token
refreshTokenModel := models.RefreshToken{
Token: refreshToken,
UserID: user.ID,
ExpiresAt: time.Now().Add(h.cfg.JWTRefreshExpiration),
}
if err := h.db.Create(&refreshTokenModel).Error; err != nil {
h.logger.Error("Failed to save refresh token", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
// Create session
session := models.Session{
ID: uuid.New().String(),
UserID: user.ID,
ExpiresAt: time.Now().Add(h.cfg.SessionExpiration),
}
if err := h.db.Create(&session).Error; err != nil {
h.logger.Error("Failed to create session", zap.Error(err))
}
// Set session cookie
c.Cookie(&fiber.Cookie{
Name: "session_id",
Value: session.ID,
Expires: session.ExpiresAt,
HTTPOnly: true,
Secure: h.cfg.Environment == "production",
SameSite: "Lax",
})
h.logger.Info("User logged in", zap.Uint("user_id", user.ID), zap.String("email", user.Email))
return c.JSON(models.TokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: "Bearer",
ExpiresIn: int(h.cfg.JWTAccessExpiration.Seconds()),
})
}
// @Summary Refresh access token
// @Description Get a new access token using refresh token
// @Tags auth
// @Accept json
// @Produce json
// @Param request body models.RefreshTokenRequest true "Refresh token"
// @Success 200 {object} models.TokenResponse
// @Failure 400 {object} handlers.ValidationErrorResponse
// @Failure 401 {object} map[string]string
// @Router /auth/refresh [post]
func (h *Handler) RefreshToken(c *fiber.Ctx) error {
var req models.RefreshTokenRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request format",
})
}
if err := h.validate.Struct(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(h.formatValidationErrors(err))
}
// Find refresh token
var refreshToken models.RefreshToken
if err := h.db.Where("token = ?", req.RefreshToken).First(&refreshToken).Error; err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid refresh token",
})
}
// Check if expired
if time.Now().After(refreshToken.ExpiresAt) {
h.db.Delete(&refreshToken)
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Refresh token expired",
})
}
// Get user
var user models.User
if err := h.db.First(&user, refreshToken.UserID).Error; err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "User not found",
})
}
// Generate new access token
accessToken, err := utils.GenerateAccessToken(user.ID, user.Email, user.Role, h.cfg.JWTSecret, h.cfg.JWTAccessExpiration)
if err != nil {
h.logger.Error("Failed to generate access token", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
return c.JSON(models.TokenResponse{
AccessToken: accessToken,
RefreshToken: req.RefreshToken,
TokenType: "Bearer",
ExpiresIn: int(h.cfg.JWTAccessExpiration.Seconds()),
})
}
// @Summary Logout user
// @Description Logout user and invalidate tokens
// @Tags auth
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {object} map[string]string
// @Router /auth/logout [post]
func (h *Handler) Logout(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
// Delete all refresh tokens
h.db.Where("user_id = ?", userID).Delete(&models.RefreshToken{})
// Delete session
sessionID := c.Cookies("session_id")
if sessionID != "" {
h.db.Where("id = ? AND user_id = ?", sessionID, userID).Delete(&models.Session{})
}
// Clear session cookie
c.Cookie(&fiber.Cookie{
Name: "session_id",
Value: "",
Expires: time.Now().Add(-time.Hour),
HTTPOnly: true,
Secure: h.cfg.Environment == "production",
SameSite: "Lax",
})
h.logger.Info("User logged out", zap.Uint("user_id", userID))
return c.JSON(fiber.Map{
"message": "Successfully logged out",
})
}
func (h *Handler) generateRefreshToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
`,
'handlers/user.go': `package handlers
import (
"strconv"
"{{projectName}}/models"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
// @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} models.PaginatedResponse
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /users [get]
func (h *Handler) ListUsers(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
limit, _ := strconv.Atoi(c.Query("limit", "10"))
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 10
}
offset := (page - 1) * limit
var users []models.User
var total int64
h.db.Model(&models.User{}).Count(&total)
if err := h.db.Offset(offset).Limit(limit).Find(&users).Error; err != nil {
h.logger.Error("Failed to fetch users", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
response := make([]models.UserResponse, len(users))
for i, user := range users {
response[i] = user.ToResponse()
}
totalPages := int(math.Ceil(float64(total) / float64(limit)))
return c.JSON(models.PaginatedResponse{
Data: response,
Total: total,
Page: page,
Limit: limit,
TotalPages: totalPages,
HasNext: page < totalPages,
HasPrev: page > 1,
})
}
// @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} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /users/{id} [get]
func (h *Handler) GetUser(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid user ID",
})
}
var user models.User
if err := h.db.First(&user, id).Error; err != nil {
return c.Status(fiber.StatusNotFound).JSON(h.notFoundError("User"))
}
return c.JSON(user.ToResponse())
}
// @Summary Get current user
// @Description Get the currently authenticated user
// @Tags users
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {object} models.UserResponse
// @Failure 401 {object} map[string]string
// @Router /users/me [get]
func (h *Handler) GetCurrentUser(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
var user models.User
if err := h.db.First(&user, userID).Error; err != nil {
return c.Status(fiber.StatusNotFound).JSON(h.notFoundError("User"))
}
return c.JSON(user.ToResponse())
}
// @Summary Update current user profile
// @Description Update the currently authenticated user's profile
// @Tags users
// @Accept json
// @Produce json
// @Security Bearer
// @Param user body models.UpdateProfileRequest true "Profile update data"
// @Success 200 {object} models.UserResponse
// @Failure 400 {object} handlers.ValidationErrorResponse
// @Failure 401 {object} map[string]string
// @Router /users/me [put]
func (h *Handler) UpdateProfile(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
var user models.User
if err := h.db.First(&user, userID).Error; err != nil {
return c.Status(fiber.StatusNotFound).JSON(h.notFoundError("User"))
}
var req models.UpdateProfileRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request format",
})
}
if err := h.validate.Struct(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(h.formatValidationErrors(err))
}
// Update fields
if req.Name != nil {
user.Name = *req.Name
}
if req.Avatar != nil {
user.Avatar = *req.Avatar
}
if req.Bio != nil {
user.Bio = *req.Bio
}
if err := h.db.Save(&user).Error; err != nil {
h.logger.Error("Failed to update user", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
h.logger.Info("User profile updated", zap.Uint("user_id", user.ID))
return c.JSON(user.ToResponse())
}
// @Summary Change password
// @Description Change the currently authenticated user's password
// @Tags users
// @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.ValidationErrorResponse
// @Failure 401 {object} map[string]string
// @Router /users/me/password [put]
func (h *Handler) ChangePassword(c *fiber.Ctx) error {
userID := c.Locals("userID").(uint)
var user models.User
if err := h.db.First(&user, userID).Error; err != nil {
return c.Status(fiber.StatusNotFound).JSON(h.notFoundError("User"))
}
var req models.ChangePasswordRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request format",
})
}
if err := h.validate.Struct(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(h.formatValidationErrors(err))
}
// Check current password
if !user.CheckPassword(req.CurrentPassword) {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Current password is incorrect",
})
}
// Set new password
if err := user.SetPassword(req.NewPassword); err != nil {
h.logger.Error("Failed to hash password", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
if err := h.db.Save(&user).Error; err != nil {
h.logger.Error("Failed to update password", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
// Invalidate all refresh tokens
h.db.Where("user_id = ?", userID).Delete(&models.RefreshToken{})
h.logger.Info("User password changed", zap.Uint("user_id", user.ID))
return c.JSON(fiber.Map{
"message": "Password changed successfully",
})
}
// @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} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /users/{id} [delete]
func (h *Handler) DeleteUser(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid user ID",
})
}
result := h.db.Delete(&models.User{}, id)
if result.Error != nil {
h.logger.Error("Failed to delete user", zap.Error(result.Error))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
if result.RowsAffected == 0 {
return c.Status(fiber.StatusNotFound).JSON(h.notFoundError("User"))
}
h.logger.Info("User deleted", zap.Uint64("user_id", id))
return c.SendStatus(fiber.StatusNoContent)
}
`,
'handlers/product.go': `package handlers
import (
"math"
"strconv"
"{{projectName}}/models"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
"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 category query string false "Filter by category"
// @Param tags query []string false "Filter by tags"
// @Param search query string false "Search in name and description"
// @Param min_price query number false "Minimum price"
// @Param max_price query number false "Maximum price"
// @Param featured query bool false "Filter featured products"
// @Param sort_by query string false "Sort by field" Enums(name, price, created_at, stock)
// @Param order query string false "Sort order" Enums(asc, desc)
// @Success 200 {object} models.PaginatedResponse
// @Router /products [get]
func (h *Handler) ListProducts(c *fiber.Ctx) error {
var req models.ProductListRequest
// Parse query parameters
if err := c.QueryParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid query parameters",
})
}
// Set defaults
if req.Page < 1 {
req.Page = 1
}
if req.Limit < 1 || req.Limit > 100 {
req.Limit = 10
}
if req.Order == "" {
req.Order = "desc"
}
if req.SortBy == "" {
req.SortBy = "created_at"
}
if err := h.validate.Struct(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(h.formatValidationErrors(err))
}
// Build query
query := h.db.Model(&models.Product{}).Where("active = ?", true)
// Apply filters
if req.Category != "" {
query = query.Where("category = ?", req.Category)
}
if len(req.Tags) > 0 {
query = query.Where("tags && ?", pq.Array(req.Tags))
}
if req.Search != "" {
searchPattern := "%" + req.Search + "%"
query = query.Where("name ILIKE ? OR description ILIKE ?", searchPattern, searchPattern)
}
if req.MinPrice > 0 {
query = query.Where("price >= ?", req.MinPrice)
}
if req.MaxPrice > 0 {
query = query.Where("price <= ?", req.MaxPrice)
}
if req.Featured {
query = query.Where("featured = ?", true)
}
// Count total
var total int64
query.Count(&total)
// Apply sorting
orderClause := req.SortBy + " " + req.Order
query = query.Order(orderClause)
// Apply pagination
offset := (req.Page - 1) * req.Limit
var products []models.Product
if err := query.Offset(offset).Limit(req.Limit).Find(&products).Error; err != nil {
h.logger.Error("Failed to fetch products", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
totalPages := int(math.Ceil(float64(total) / float64(req.Limit)))
return c.JSON(models.PaginatedResponse{
Data: products,
Total: total,
Page: req.Page,
Limit: req.Limit,
TotalPages: totalPages,
HasNext: req.Page < totalPages,
HasPrev: req.Page > 1,
})
}
// @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} map[string]string
// @Failure 404 {object} map[string]string
// @Router /products/{id} [get]
func (h *Handler) GetProduct(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid product ID",
})
}
var product models.Product
if err := h.db.First(&product, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return c.Status(fiber.StatusNotFound).JSON(h.notFoundError("Product"))
}
h.logger.Error("Failed to fetch product", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
return c.JSON(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.ValidationErrorResponse
// @Failure 401 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Router /products [post]
func (h *Handler) CreateProduct(c *fiber.Ctx) error {
var req models.CreateProductRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request format",
})
}
if err := h.validate.Struct(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(h.formatValidationErrors(err))
}
// Check if SKU exists
var existingProduct models.Product
if err := h.db.Where("sku = ?", req.SKU).First(&existingProduct).Error; err == nil {
return c.Status(fiber.StatusConflict).JSON(fiber.Map{
"error": "Product with this SKU already exists",
})
}
product := models.Product{
Name: req.Name,
Description: req.Description,
Price: req.Price,
Stock: req.Stock,
SKU: req.SKU,
Category: req.Category,
Tags: req.Tags,
Images: req.Images,
Active: true,
}
if err := h.db.Create(&product).Error; err != nil {
h.logger.Error("Failed to create product", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
h.logger.Info("Product created", zap.Uint("product_id", product.ID), zap.String("sku", product.SKU))
return c.Status(fiber.StatusCreated).JSON(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.ValidationErrorResponse
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /products/{id} [put]
func (h *Handler) UpdateProduct(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid product ID",
})
}
var product models.Product
if err := h.db.First(&product, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return c.Status(fiber.StatusNotFound).JSON(h.notFoundError("Product"))
}
h.logger.Error("Failed to fetch product", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
var req models.UpdateProductRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request format",
})
}
if err := h.validate.Struct(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(h.formatValidationErrors(err))
}
// 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.Stock != nil {
product.Stock = *req.Stock
}
if req.Category != nil {
product.Category = *req.Category
}
if req.Tags != nil {
product.Tags = *req.Tags
}
if req.Images != nil {
product.Images = *req.Images
}
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 {
h.logger.Error("Failed to update product", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
h.logger.Info("Product updated", zap.Uint("product_id", product.ID))
return c.JSON(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} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /products/{id} [delete]
func (h *Handler) DeleteProduct(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid product ID",
})
}
result := h.db.Delete(&models.Product{}, id)
if result.Error != nil {
h.logger.Error("Failed to delete product", zap.Error(result.Error))
return c.Status(fiber.StatusInternalServerError).JSON(h.internalError())
}
if result.RowsAffected == 0 {
return c.Status(fiber.StatusNotFound).JSON(h.notFoundError("Product"))
}
h.logger.Info("Product deleted", zap.Uint64("product_id", id))
return c.SendStatus(fiber.StatusNoContent)
}
`,
'handlers/websocket.go': `package handlers
import (
"encoding/json"
"time"
"github.com/gofiber/websocket/v2"
"go.uber.org/zap"
)
type WebSocketMessage struct {
Type string \`json:"type"\`
Payload json.RawMessage \`json:"payload"\`
}
type WebSocketClient struct {
Conn *websocket.Conn
UserID uint
Send chan WebSocketMessage
}
var clients = make(map[uint]*WebSocketClient)
var broadcast = make(chan WebSocketMessage)
// @Summary WebSocket endpoint
// @Description WebSocket connection for real-time communication
// @Tags websocket
// @Security Bearer
// @Router /ws [get]
func (h *Handler) WebSocketHandler(c *websocket.Conn) {
userID := c.Locals("userID").(uint)
client := &WebSocketClient{
Conn: c,
UserID: userID,
Send: make(chan WebSocketMessage, 256),
}
clients[userID] = client
defer func() {
delete(clients, userID)
close(client.Send)
}()
h.logger.Info("WebSocket connection established", zap.Uint("user_id", userID))
// Send welcome message
welcome := WebSocketMessage{
Type: "welcome",
Payload: json.RawMessage(\`{"message":"Connected to WebSocket","user_id":\` + strconv.Itoa(int(userID)) + \`}\`),
}
if err := c.WriteJSON(welcome); err != nil {
h.logger.Error("Failed to send welcome message", zap.Error(err))
return
}
// Start goroutines for reading and writing
go client.writePump(h.logger)
client.readPump(h.logger)
}
func (c *WebSocketClient) readPump(logger *zap.Logger) {
defer c.Conn.Close()
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
c.Conn.SetPongHandler(func(string) error {
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
for {
var msg WebSocketMessage
if err := c.Conn.ReadJSON(&msg); err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
logger.Error("WebSocket error", zap.Error(err))
}
break
}
logger.Debug("Received WebSocket message",
zap.Uint("user_id", c.UserID),
zap.String("type", msg.Type))
// Handle different message types
switch msg.Type {
case "ping":
c.Send <- WebSocketMessage{
Type: "pong",
Payload: msg.Payload,
}
case "broadcast":
// Broadcast message to all connected clients
broadcast <- msg
default:
c.Send <- WebSocketMessage{
Type: "error",
Payload: json.RawMessage(\`{"message":"Unknown message type"}\`),
}
}
}
}
func (c *WebSocketClient) writePump(logger *zap.Logger) {
ticker := time.NewTicker(54 * time.Second)
defer func() {
ticker.Stop()
c.Conn.Close()
}()
for {
select {
case message, ok := <-c.Send:
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := c.Conn.WriteJSON(message); err != nil {
logger.Error("Failed to write message", zap.Error(err))
return
}
case <-ticker.C:
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
// Broadcast handler
func init() {
go func() {
for {
msg := <-broadcast
for _, client := range clients {
select {
case client.Send <- msg:
default:
close(client.Send)
delete(clients, client.UserID)
}
}
}
}()
}
`,
// Routes
'routes/routes.go': `package routes
import (
"{{projectName}}/config"
"{{projectName}}/handlers"
"{{projectName}}/middleware"
"github.com/gofiber/fiber/v2"
"githu