@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,786 lines (1,526 loc) • 57.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.echoTemplate = void 0;
exports.echoTemplate = {
id: 'echo',
name: 'echo',
displayName: 'Echo Framework',
description: 'High performance, minimalist Go web framework with automatic TLS and HTTP/2 support',
language: 'go',
framework: 'echo',
version: '4.11.4',
tags: ['go', 'echo', 'api', 'rest', 'validation', 'performance', 'http2'],
port: 8080,
dependencies: {},
features: ['authentication', 'validation', 'logging', 'cors', 'documentation', 'websockets'],
files: {
// Go module configuration
'go.mod': `module {{projectName}}
go 1.21
require (
github.com/labstack/echo/v4 v4.11.4
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/echo-swagger v1.4.1
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
golang.org/x/time v0.5.0
github.com/gorilla/websocket v1.5.1
)
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/cespare/xxhash/v2 v2.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/ghodss/yaml v1.0.0 // 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/labstack/gommon v0.4.2 // 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-sqlite3 v1.14.19 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // 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.v2 v2.4.0 // 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"
"time"
"{{projectName}}/config"
"{{projectName}}/database"
_ "{{projectName}}/docs" // swagger docs
"{{projectName}}/handlers"
"{{projectName}}/middleware"
"github.com/joho/godotenv"
"github.com/labstack/echo/v4"
echoMiddleware "github.com/labstack/echo/v4/middleware"
echoSwagger "github.com/swaggo/echo-swagger"
"go.uber.org/zap"
)
// @title {{projectName}} API
// @version 1.0
// @description API server for {{projectName}} built with Echo 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: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 {
fmt.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 Echo instance
e := echo.New()
e.HideBanner = true
e.HidePort = true
// Custom error handler
e.HTTPErrorHandler = middleware.CustomHTTPErrorHandler
// Global middleware
e.Use(echoMiddleware.RequestIDWithConfig(echoMiddleware.RequestIDConfig{
Generator: middleware.GenerateRequestID,
}))
e.Use(middleware.ZapLogger(logger))
e.Use(echoMiddleware.Recover())
e.Use(echoMiddleware.CORSWithConfig(echoMiddleware.CORSConfig{
AllowOrigins: cfg.AllowedOrigins,
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodOptions},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization, "X-Request-ID"},
ExposeHeaders: []string{"X-Request-ID"},
AllowCredentials: true,
MaxAge: 86400,
}))
e.Use(echoMiddleware.GzipWithConfig(echoMiddleware.GzipConfig{
Level: 5,
}))
e.Use(middleware.RateLimiter(cfg))
e.Use(echoMiddleware.SecureWithConfig(echoMiddleware.SecureConfig{
XSSProtection: "1; mode=block",
ContentTypeNosniff: "nosniff",
XFrameOptions: "DENY",
HSTSMaxAge: 3600,
ContentSecurityPolicy: "default-src 'self'",
}))
// Health check
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"status": "healthy",
"time": time.Now().UTC(),
})
})
// Swagger documentation
e.GET("/swagger/*", echoSwagger.WrapHandler)
// Initialize handlers
h := handlers.NewHandler(db, cfg, logger)
// API routes
api := e.Group("/api/v1")
{
// Auth routes
auth := api.Group("/auth")
auth.POST("/register", h.Register)
auth.POST("/login", h.Login)
auth.POST("/refresh", h.RefreshToken)
// User routes
users := api.Group("/users")
users.Use(middleware.JWT(cfg.JWTSecret))
users.GET("", h.ListUsers, middleware.RequireRole("admin"))
users.GET("/:id", h.GetUser)
users.GET("/me", h.GetCurrentUser)
users.PUT("/me", h.UpdateCurrentUser)
users.DELETE("/:id", h.DeleteUser, middleware.RequireRole("admin"))
// Product routes
products := api.Group("/products")
products.GET("", h.ListProducts)
products.GET("/:id", h.GetProduct)
products.Use(middleware.JWT(cfg.JWTSecret))
products.POST("", h.CreateProduct, middleware.RequireRole("admin"))
products.PUT("/:id", h.UpdateProduct, middleware.RequireRole("admin"))
products.DELETE("/:id", h.DeleteProduct, middleware.RequireRole("admin"))
// WebSocket endpoint
ws := api.Group("/ws")
ws.Use(middleware.JWT(cfg.JWTSecret))
ws.GET("", h.WebSocketHandler)
}
// Start server
go func() {
address := fmt.Sprintf(":%d", cfg.Port)
logger.Info("Starting server", zap.String("address", address))
if cfg.TLSEnabled {
if err := e.StartTLS(address, cfg.TLSCertFile, cfg.TLSKeyFile); err != nil && err != http.ErrServerClosed {
logger.Fatal("Failed to start TLS server", zap.Error(err))
}
} else {
if err := e.Start(address); err != nil && err != http.ErrServerClosed {
logger.Fatal("Failed to start server", zap.Error(err))
}
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
// Graceful shutdown
logger.Info("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(ctx); err != nil {
logger.Fatal("Server forced to shutdown", zap.Error(err))
}
logger.Info("Server exited")
}
`,
// 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
JWTAccessExpiration time.Duration
JWTRefreshExpiration time.Duration
// Redis
RedisAddr string
RedisPassword string
RedisDB int
// Rate limiting
RateLimitRequests int
RateLimitDuration time.Duration
// CORS
AllowedOrigins []string
// TLS
TLSEnabled bool
TLSCertFile string
TLSKeyFile string
}
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"),
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: strings.Split(getEnv("ALLOWED_ORIGINS", "*"), ","),
// TLS
TLSEnabled: getEnvAsBool("TLS_ENABLED", false),
TLSCertFile: getEnv("TLS_CERT_FILE", ""),
TLSKeyFile: getEnv("TLS_KEY_FILE", ""),
}
}
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,
}
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)
}
return db, nil
}
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(
&models.User{},
&models.Product{},
&models.RefreshToken{},
)
}
`,
// 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"\`
Role string \`gorm:"default:user" json:"role" validate:"omitempty,oneof=user admin"\`
Active bool \`gorm:"default:true" json:"active"\`
RefreshTokens []RefreshToken \`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"\`
}
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 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"\`
Role string \`json:"role"\`
Active bool \`json:"active"\`
CreatedAt time.Time \`json:"created_at"\`
}
func (u *User) ToResponse() UserResponse {
return UserResponse{
ID: u.ID,
Email: u.Email,
Name: u.Name,
Role: u.Role,
Active: u.Active,
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" 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 \`json:"category" validate:"required,min=1,max=100"\`
Active bool \`gorm:"default:true" json:"active"\`
}
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"\`
}
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"\`
Active *bool \`json:"active,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"\`
Search string \`query:"search" validate:"omitempty,min=1,max=100"\`
SortBy string \`query:"sort_by" validate:"omitempty,oneof=name price created_at"\`
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"\`
}
`,
// 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 {
return &Handler{
db: db,
cfg: cfg,
logger: logger,
validate: validator.New(),
}
}
// Custom validation errors
type ValidationError struct {
Field string \`json:"field"\`
Message string \`json:"message"\`
}
func (h *Handler) formatValidationErrors(err error) []ValidationError {
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 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":
return "Value is too short"
case "max":
return "Value is too long"
case "oneof":
return "Invalid value"
default:
return "Invalid value"
}
}
`,
'handlers/auth.go': `package handlers
import (
"crypto/rand"
"encoding/base64"
"net/http"
"time"
"{{projectName}}/models"
"{{projectName}}/utils"
"github.com/labstack/echo/v4"
"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} map[string]interface{}
// @Failure 409 {object} map[string]interface{}
// @Router /auth/register [post]
func (h *Handler) Register(c echo.Context) error {
var req models.RegisterRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request format")
}
if err := h.validate.Struct(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"error": "Validation failed",
"errors": 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 echo.NewHTTPError(http.StatusConflict, "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 echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user")
}
if err := h.db.Create(&user).Error; err != nil {
h.logger.Error("Failed to create user", zap.Error(err))
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user")
}
h.logger.Info("User registered", zap.Uint("user_id", user.ID), zap.String("email", user.Email))
return c.JSON(http.StatusCreated, 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} map[string]interface{}
// @Failure 401 {object} map[string]interface{}
// @Router /auth/login [post]
func (h *Handler) Login(c echo.Context) error {
var req models.LoginRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request format")
}
if err := h.validate.Struct(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"error": "Validation failed",
"errors": h.formatValidationErrors(err),
})
}
// Find user
var user models.User
if err := h.db.Where("email = ?", req.Email).First(&user).Error; err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
}
// Check password
if !user.CheckPassword(req.Password) {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
}
// Check if user is active
if !user.Active {
return echo.NewHTTPError(http.StatusUnauthorized, "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 echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate token")
}
refreshToken, err := h.generateRefreshToken()
if err != nil {
h.logger.Error("Failed to generate refresh token", zap.Error(err))
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate token")
}
// 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 echo.NewHTTPError(http.StatusInternalServerError, "Failed to save token")
}
h.logger.Info("User logged in", zap.Uint("user_id", user.ID), zap.String("email", user.Email))
return c.JSON(http.StatusOK, 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} map[string]interface{}
// @Failure 401 {object} map[string]interface{}
// @Router /auth/refresh [post]
func (h *Handler) RefreshToken(c echo.Context) error {
var req models.RefreshTokenRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request format")
}
if err := h.validate.Struct(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"error": "Validation failed",
"errors": h.formatValidationErrors(err),
})
}
// Find refresh token
var refreshToken models.RefreshToken
if err := h.db.Where("token = ?", req.RefreshToken).First(&refreshToken).Error; err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid refresh token")
}
// Check if expired
if time.Now().After(refreshToken.ExpiresAt) {
h.db.Delete(&refreshToken)
return echo.NewHTTPError(http.StatusUnauthorized, "Refresh token expired")
}
// Get user
var user models.User
if err := h.db.First(&user, refreshToken.UserID).Error; err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "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 echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate token")
}
return c.JSON(http.StatusOK, models.TokenResponse{
AccessToken: accessToken,
RefreshToken: req.RefreshToken,
TokenType: "Bearer",
ExpiresIn: int(h.cfg.JWTAccessExpiration.Seconds()),
})
}
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
}
// Clean up expired refresh tokens periodically
func (h *Handler) CleanupExpiredTokens() {
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for range ticker.C {
result := h.db.Where("expires_at < ?", time.Now()).Delete(&models.RefreshToken{})
if result.Error != nil {
h.logger.Error("Failed to cleanup expired tokens", zap.Error(result.Error))
} else {
h.logger.Info("Cleaned up expired tokens", zap.Int64("count", result.RowsAffected))
}
}
}
`,
'handlers/user.go': `package handlers
import (
"net/http"
"strconv"
"{{projectName}}/models"
"github.com/labstack/echo/v4"
"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
// @Success 200 {array} models.UserResponse
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /users [get]
func (h *Handler) ListUsers(c echo.Context) error {
var users []models.User
if err := h.db.Find(&users).Error; err != nil {
h.logger.Error("Failed to fetch users", zap.Error(err))
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch users")
}
response := make([]models.UserResponse, len(users))
for i, user := range users {
response[i] = user.ToResponse()
}
return c.JSON(http.StatusOK, 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} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /users/{id} [get]
func (h *Handler) GetUser(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID")
}
var user models.User
if err := h.db.First(&user, id).Error; err != nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
return c.JSON(http.StatusOK, 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 echo.Context) error {
userID := c.Get("userID").(uint)
var user models.User
if err := h.db.First(&user, userID).Error; err != nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
return c.JSON(http.StatusOK, user.ToResponse())
}
// @Summary Update current user
// @Description Update the currently authenticated user's profile
// @Tags users
// @Accept json
// @Produce json
// @Security Bearer
// @Param user body map[string]string true "User update data"
// @Success 200 {object} models.UserResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /users/me [put]
func (h *Handler) UpdateCurrentUser(c echo.Context) error {
userID := c.Get("userID").(uint)
var user models.User
if err := h.db.First(&user, userID).Error; err != nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
var updates map[string]interface{}
if err := c.Bind(&updates); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request format")
}
// Remove fields that shouldn't be updated
delete(updates, "id")
delete(updates, "email")
delete(updates, "role")
delete(updates, "password")
if err := h.db.Model(&user).Updates(updates).Error; err != nil {
h.logger.Error("Failed to update user", zap.Error(err))
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to update user")
}
h.logger.Info("User updated", zap.Uint("user_id", user.ID))
return c.JSON(http.StatusOK, 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} 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 echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "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 echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete user")
}
if result.RowsAffected == 0 {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
h.logger.Info("User deleted", zap.Uint64("user_id", id))
return c.NoContent(http.StatusNoContent)
}
`,
'handlers/product.go': `package handlers
import (
"math"
"net/http"
"strconv"
"{{projectName}}/models"
"github.com/labstack/echo/v4"
"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 search query string false "Search in name and description"
// @Param sort_by query string false "Sort by field" Enums(name, price, created_at)
// @Param order query string false "Sort order" Enums(asc, desc)
// @Success 200 {object} models.PaginatedResponse
// @Router /products [get]
func (h *Handler) ListProducts(c echo.Context) error {
var req models.ProductListRequest
// Set defaults
req.Page = 1
req.Limit = 10
req.Order = "desc"
req.SortBy = "created_at"
// Bind query parameters
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid query parameters")
}
if err := h.validate.Struct(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"error": "Validation failed",
"errors": 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 req.Search != "" {
searchPattern := "%" + req.Search + "%"
query = query.Where("name LIKE ? OR description LIKE ?", searchPattern, searchPattern)
}
// 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 echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch products")
}
totalPages := int(math.Ceil(float64(total) / float64(req.Limit)))
return c.JSON(http.StatusOK, models.PaginatedResponse{
Data: products,
Total: total,
Page: req.Page,
Limit: req.Limit,
TotalPages: totalPages,
})
}
// @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 echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid product ID")
}
var product models.Product
if err := h.db.First(&product, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return echo.NewHTTPError(http.StatusNotFound, "Product not found")
}
h.logger.Error("Failed to fetch product", zap.Error(err))
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch product")
}
return c.JSON(http.StatusOK, 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} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Router /products [post]
func (h *Handler) CreateProduct(c echo.Context) error {
var req models.CreateProductRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request format")
}
if err := h.validate.Struct(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"error": "Validation failed",
"errors": 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 echo.NewHTTPError(http.StatusConflict, "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,
Active: true,
}
if err := h.db.Create(&product).Error; err != nil {
h.logger.Error("Failed to create product", zap.Error(err))
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create product")
}
h.logger.Info("Product created", zap.Uint("product_id", product.ID), zap.String("sku", product.SKU))
return c.JSON(http.StatusCreated, 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} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /products/{id} [put]
func (h *Handler) UpdateProduct(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid product ID")
}
var product models.Product
if err := h.db.First(&product, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return echo.NewHTTPError(http.StatusNotFound, "Product not found")
}
h.logger.Error("Failed to fetch product", zap.Error(err))
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch product")
}
var req models.UpdateProductRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request format")
}
if err := h.validate.Struct(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"error": "Validation failed",
"errors": 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.Active != nil {
product.Active = *req.Active
}
if err := h.db.Save(&product).Error; err != nil {
h.logger.Error("Failed to update product", zap.Error(err))
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to update product")
}
h.logger.Info("Product updated", zap.Uint("product_id", product.ID))
return c.JSON(http.StatusOK, 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 echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "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 echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete product")
}
if result.RowsAffected == 0 {
return echo.NewHTTPError(http.StatusNotFound, "Product not found")
}
h.logger.Info("Product deleted", zap.Uint64("product_id", id))
return c.NoContent(http.StatusNoContent)
}
`,
'handlers/websocket.go': `package handlers
import (
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
type WebSocketMessage struct {
Type string \`json:"type"\`
Payload interface{} \`json:"payload"\`
}
// @Summary WebSocket endpoint
// @Description WebSocket connection for real-time communication
// @Tags websocket
// @Security Bearer
// @Router /ws [get]
func (h *Handler) WebSocketHandler(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
h.logger.Error("Failed to upgrade connection", zap.Error(err))
return err
}
defer ws.Close()
userID := c.Get("userID").(uint)
h.logger.Info("WebSocket connection established", zap.Uint("user_id", userID))
// Send welcome message
welcome := WebSocketMessage{
Type: "welcome",
Payload: map[string]interface{}{
"message": "Connected to WebSocket",
"user_id": userID,
},
}
if err := ws.WriteJSON(welcome); err != nil {
h.logger.Error("Failed to send welcome message", zap.Error(err))
return err
}
// Message handling loop
for {
var msg WebSocketMessage
if err := ws.ReadJSON(&msg); err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
h.logger.Error("WebSocket error", zap.Error(err))
}
break
}
h.logger.Debug("Received WebSocket message",
zap.Uint("user_id", userID),
zap.String("type", msg.Type))
// Handle different message types
switch msg.Type {
case "ping":
response := WebSocketMessage{
Type: "pong",
Payload: msg.Payload,
}
if err := ws.WriteJSON(response); err != nil {
h.logger.Error("Failed to send pong", zap.Error(err))
break
}
case "echo":
response := WebSocketMessage{
Type: "echo_response",
Payload: msg.Payload,
}
if err := ws.WriteJSON(response); err != nil {
h.logger.Error("Failed to send echo response", zap.Error(err))
break
}
default:
response := WebSocketMessage{
Type: "error",
Payload: map[string]string{
"message": "Unknown message type",
},
}
if err := ws.WriteJSON(response); err != nil {
h.logger.Error("Failed to send error response", zap.Error(err))
break
}
}
}
h.logger.Info("WebSocket connection closed", zap.Uint("user_id", userID))
return nil
}
`,
// Middleware
'middleware/auth.go': `package middleware
import (
"strings"
"{{projectName}}/utils"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func JWT(secret string) echo.MiddlewareFunc {
return middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte(secret),
TokenLookup: "header:Authorization",
AuthScheme: "Bearer",
Claims: &utils.JWTClaims{},
ErrorHandler: func(err error) error {
return echo.NewHTTPError(401, "Invalid or expired token")
},
SuccessHandler: func(c echo.Context) {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*utils.JWTClaims)
c.Set("userID", claims.UserID)
c.Set("userEmail", claims.Email)
c.Set("userRole", claims.Role)
},
})
}
func RequireRole(roles ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
userRole, ok := c.Get("userRole").(string)
if !ok {
return echo.NewHTTPError(403, "Access denied")
}
for _, role := range roles {
if userRole == role {
return next(c)
}
}
return echo.NewHTTPError(403, "Insufficient permissions")
}
}
}
func ExtractToken(authHeader string) (string, error) {
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
return "", echo.NewHTTPError(401, "Invalid authorization header format")
}
return parts[1], nil
}
`,
'middleware/logger.go': `package middleware
import (
"time"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
)
func ZapLogger(logger *zap.Logger) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
start := time.Now()
err := next(c)
if err != nil {
c.Error(err)
}
req := c.Request()
res := c.Response()
fields := []zap.Field{
zap.String("remote_ip", c.RealIP()),
zap.String("host", req.Host),
zap.String("method", req.Method),
zap.String("uri", req.RequestURI),
zap.Int("status", res.Status),
zap.Int64("size", res.Size),
zap.String("user_agent", req.UserAgent()),
zap.Duration("latency", time.Since(start)),
zap.String("request_id", c.Request().Header.Get(echo.HeaderXRequestID)),
}
// Add user ID if authenticated
if userID, ok := c.Get("userID").(uint); ok {
fields = append(fields, zap.Uint("user_id", userID))
}
// Log based on status code
switch {
case res.Status >= 500:
logger.Error("Server error", fields...)
case res.Status >= 400:
logger.Warn("Client error", fields...)
case res.Status >= 300:
logger.Info("Redirect", fields...)
default:
logger.Info("Success", fields...)
}
return err
}
}
}
`,
'middleware/rate_limiter.go': `package middleware
import (
"fmt"
"net/http"
"time"
"{{projectName}}/config"
"github.com/labstack/echo/v4"
"github.com/redis/go-redis/v9"
"golang.org/x/time/rate"
)
var limiters = make(map[string]*rate.Limiter)
func RateLimiter(cfg *config.Config) echo.MiddlewareFunc {
// Try to use Redis for distributed rate limiting
rdb := redis.NewClient(&redis.Options{
Addr: cfg.RedisAddr,
Password: cfg.RedisPassword,
DB: cfg.RedisDB,
})
// Test Redis connection
ctx := context.Background()
_, err := rdb.Ping(ctx).Result()
useRedis := err == nil
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ip := c.RealIP()
key := fmt.Sprintf("rate_limit:%s", ip)
if useRedis {
// Redis-based rate limiting
pipe := rdb.Pipeline()
incr := pipe.Incr(ctx, key)
pipe.Expire(ctx, key, cfg.RateLimitDuration)
_, err := pipe.Exec(ctx)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Rate limiter error")
}
count := incr.Val()
if count > int64(cfg.RateLimitRequests) {
return echo.NewHTTPError(http.StatusTooManyRequests, "Rate limit exceeded")
}
// Set rate limit headers
c.Response().Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", cfg.RateLimitRequests))
c.Response().Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", cfg.RateLimitRequests-int(count)))
c.Response().Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(cfg.RateLimitDuration).Unix()))
} else {
// In-memory rate limiting fallback
limiter, exists := limiters[ip]
if !exists {
limiter = rate.NewLimiter(rate.Every(cfg.RateLimitDuration/time.Duration(cfg.RateLimitRequests)), cfg.RateLimitRequests)
limiters[ip] = limiter
}
if !limiter.Allow() {
return echo.NewHTTPError(http.StatusTooManyRequests, "Rate limit exceeded")
}
}
return next(c)
}
}
}
`,
'middleware/error_handler.go': `package middleware
import (
"net/http"
"github.com/labstack/echo/v4"
)
func CustomHTTPErrorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
message := "Internal server error"
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = fmt.Sprintf("%v", he.Message)
}
// Don't expose internal error details in production
if code == http.StatusInternalServerError && c.Echo().Debug == false {
message = "Internal server error"
}
// Send response
if !c.Response().Committed {
if c.Request().Method == http.MethodHead {
c.NoContent(code)
} else {
c.JSON(code, map[string]interface{}{
"error": message,
"code": code,
})
}
}
}
`,
'middleware/request_id.go': `package middleware
import (
"github.com/google/uuid"
)
func GenerateRequestID() string {
return uuid.New().String()
}
`,
// Utils
'utils/jwt.go': `package utils
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
type JWTClaims struct {
UserID uint \`json:"user_id"\`
Email string \`json:"email"\`
Role string \`json:"role"\`
jwt.RegisteredClaims
}
func GenerateAccessToken(userID uint, email, role, secret string, expiration time.Duration) (string, error) {
claims := JWTClaims{
UserID: userID,
Email: email,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
func ValidateToken(tokenString, secret string) (*JWTClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*JWTClaims)
if !ok || !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
`,
// Test files
'handlers/auth_test.go': `package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"{{projectName}}/config"
"{{projectName}}/database"
"{{projectName}}/models"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupTestDB() (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
return nil, err
}
err = database.Migrate(db)
if err != nil {
return nil, err
}
return db, nil
}
func TestRegister(t *testing.T) {
// Setup
e := echo.New()
db, err := setupTestDB()
assert.NoError(t, err)
cfg := &config.Config{
JWTSecret: "test-secret",
JWTAccessExpiration: time.Hour,
}
logger, _ := zap.NewDevelopment()
h := NewHandler(db, cfg, logger)
// Test successful registration
user := models.RegisterRequest{
Email: "test@example.com",
Password: "password123",
Name: "Test User",
}
body, _ := json.Marshal(user)
req := httptest.NewRequest(http.MethodPost, "/auth/register", bytes.NewReader(body))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err = h.Register(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, rec.Code)
var response models.UserResponse
err = json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, user.Email, response.Email)
assert.Equal(t, user.Name, response.Name)
// Test duplicate email
req = httptest.NewRequest(http.MethodPost, "/auth/register", bytes.NewReader(body))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec = httptest.NewRecorder()
c = e.NewContext(req, rec)
err = h.Register(c)
assert.Error(t, err)
httpError := err.(*echo.HTTPError)
assert.Equal(t, http.StatusConflict, httpError.Code)
}
func TestLogin(t *testing.T) {
// Setup
e := echo.New()
db, err := setupTestDB()
assert.NoError(t, err)
cfg := &config.Config{
JWTSecret: "test-secret",
JWTAccessExpiration: time.Hour,
JWTRefreshExpiration: 24 * time.Hour,
}
logger, _ := zap.NewDevelopment()
h := NewHandler(db, cfg, logger)
// Create test user
user := models.User{
Email: "test@example.com",
Name: "Test User",
}
user.SetPassword("password123")
db.Create(&user)
// Test successful login
loginReq := models.LoginRequest{
Email: "test@example.com",
Password: "password123",
}
body, _ := json.Marshal(loginReq)
req := httptest.N