package server import ( "strconv" "time" "a301_server/pkg/apperror" "a301_server/pkg/config" "a301_server/pkg/metrics" "a301_server/pkg/middleware" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) // New creates a configured Fiber app with all global middleware applied. func New() *fiber.App { app := fiber.New(fiber.Config{ StreamRequestBody: true, BodyLimit: 4 * 1024 * 1024 * 1024, // 4GB ErrorHandler: middleware.ErrorHandler, }) app.Use(recover.New(recover.Config{ EnableStackTrace: true, })) app.Use(middleware.RequestID) app.Use(middleware.Metrics) app.Get("/metrics", metrics.Handler) app.Use(logger.New(logger.Config{ Format: `{"time":"${time}","status":${status},"latency":"${latency}","method":"${method}","path":"${path}","ip":"${ip}","reqId":"${locals:requestID}"}` + "\n", TimeFormat: "2006-01-02T15:04:05Z07:00", })) app.Use(middleware.SecurityHeaders) app.Use(cors.New(cors.Config{ AllowOrigins: config.C.CORSAllowOrigins, AllowHeaders: "Origin, Content-Type, Authorization, Idempotency-Key, X-API-Key, X-Requested-With", AllowMethods: "GET, POST, PUT, PATCH, DELETE", ExposeHeaders: "X-Request-ID, X-Idempotent-Replay", AllowCredentials: true, })) return app } // AuthLimiter returns a rate limiter for auth endpoints (10 req/min per IP). func AuthLimiter() fiber.Handler { return limiter.New(limiter.Config{ Max: 10, Expiration: 1 * time.Minute, KeyGenerator: func(c *fiber.Ctx) string { return c.IP() }, LimitReached: func(c *fiber.Ctx) error { return apperror.ErrRateLimited }, }) } // APILimiter returns a rate limiter for general API endpoints (120 req/min per IP). func APILimiter() fiber.Handler { return limiter.New(limiter.Config{ Max: 120, Expiration: 1 * time.Minute, KeyGenerator: func(c *fiber.Ctx) string { return c.IP() }, LimitReached: func(c *fiber.Ctx) error { return apperror.ErrRateLimited }, }) } // RefreshLimiter returns a rate limiter for refresh token endpoint (5 req/min per IP). // Separate from AuthLimiter to avoid NAT collisions while still preventing abuse. func RefreshLimiter() fiber.Handler { return limiter.New(limiter.Config{ Max: 5, Expiration: 1 * time.Minute, KeyGenerator: func(c *fiber.Ctx) string { return "refresh:" + c.IP() }, LimitReached: func(c *fiber.Ctx) error { return apperror.ErrRateLimited }, }) } // ChainUserLimiter returns a rate limiter for chain transactions (20 req/min per user). func ChainUserLimiter() fiber.Handler { return limiter.New(limiter.Config{ Max: 20, Expiration: 1 * time.Minute, KeyGenerator: func(c *fiber.Ctx) string { if uid, ok := c.Locals("userID").(uint); ok { return "chain_user:" + strconv.FormatUint(uint64(uid), 10) } return "chain_ip:" + c.IP() }, LimitReached: func(c *fiber.Ctx) error { return apperror.ErrRateLimited }, }) } // HealthCheck returns a handler that reports server liveness. func HealthCheck() fiber.Handler { return func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"status": "ok"}) } } // ReadyCheck returns a handler that verifies DB and Redis connectivity. func ReadyCheck(db *gorm.DB, rdb *redis.Client) fiber.Handler { return func(c *fiber.Ctx) error { sqlDB, err := db.DB() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "db pool"}) } if err := sqlDB.Ping(); err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "db"}) } if err := rdb.Ping(c.Context()).Err(); err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "redis"}) } return c.JSON(fiber.Map{"status": "ok"}) } }