CRITICAL: - graceful shutdown 레이스 수정 — Listen을 goroutine으로 이동 - Register 레이스 컨디션 — sentinel error + MySQL duplicate key 처리 HIGH: - 멱등성 키에 method+path 포함 — 엔드포인트 간 캐시 충돌 방지 - 입장 토큰 생성 실패 시 방/슬롯 롤백 추가 MEDIUM: - RequestEntry 슬롯 없음 시 503 반환 - chain ExportWallet/GetWalletInfo/GrantReward 에러 처리 개선 - resolveUsername 에러 타입 구분 (duplicate key vs 기타) - 공지사항 길이 검증 byte→rune (한국어 256자 허용) - Level 검증 범위 MaxLevel(50)로 통일 - admin 자기 강등 방지 - CORS ExposeHeaders 추가 - MySQL DSN loc=Local→loc=UTC - hashGameExeFromZip 100MB 초과 절단 감지 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
132 lines
3.9 KiB
Go
132 lines
3.9 KiB
Go
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"})
|
|
}
|
|
}
|