feat: 코드 리뷰 기반 전면 개선 — 보안, 검증, 테스트, 안정성
- 체인 nonce 경쟁 조건 수정 (operatorMu + per-user mutex) - 등록/SSAFY 원자적 트랜잭션 (wallet+profile 롤백 보장) - IdempotencyRequired 미들웨어 (SETNX 원자적 클레임) - 런치 티켓 API (JWT URL 노출 방지) - HttpOnly 쿠키 refresh token - SSAFY OAuth state 파라미터 (CSRF 방지) - Refresh 시 DB 조회로 최신 role 사용 - 공지사항/유저목록 페이지네이션 - BodyLimit 미들웨어 (1MB, upload 제외) - 입력 검증 강화 (닉네임, 게임데이터, 공지 길이) - 에러 메시지 내부 정보 노출 방지 - io.LimitReader (RPC 10MB, SSAFY 1MB) - RequestID 비출력 문자 제거 - 단위 테스트 (auth 11, announcement 9, bossraid 16) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,9 @@ func Register(
|
||||
app.Get("/health", healthCheck)
|
||||
app.Get("/ready", readyCheck)
|
||||
|
||||
api := app.Group("/api", apiLimiter)
|
||||
// Default 1MB body limit for API routes; upload endpoints are excluded
|
||||
apiBodyLimit := middleware.BodyLimit(1*1024*1024, "/api/download/upload")
|
||||
api := app.Group("/api", apiLimiter, apiBodyLimit)
|
||||
|
||||
// Auth
|
||||
a := api.Group("/auth")
|
||||
@@ -37,9 +39,11 @@ func Register(
|
||||
a.Post("/login", authLimiter, authH.Login)
|
||||
a.Post("/refresh", authLimiter, authH.Refresh)
|
||||
a.Post("/logout", middleware.Auth, authH.Logout)
|
||||
a.Post("/verify", authLimiter, authH.VerifyToken)
|
||||
// /verify moved to internal API (ServerAuth) — see internal section below
|
||||
a.Get("/ssafy/login", authH.SSAFYLoginURL)
|
||||
a.Post("/ssafy/callback", authLimiter, authH.SSAFYCallback)
|
||||
a.Post("/launch-ticket", middleware.Auth, authH.CreateLaunchTicket)
|
||||
a.Post("/redeem-ticket", authLimiter, authH.RedeemLaunchTicket)
|
||||
|
||||
// Users (admin only)
|
||||
u := api.Group("/users", middleware.Auth, middleware.AdminOnly)
|
||||
@@ -73,19 +77,19 @@ func Register(
|
||||
ch.Get("/market/:id", chainH.GetMarketListing)
|
||||
|
||||
// Chain - User Transactions (authenticated, per-user rate limited, idempotency-protected)
|
||||
ch.Post("/transfer", chainUserLimiter, middleware.Idempotency, chainH.Transfer)
|
||||
ch.Post("/asset/transfer", chainUserLimiter, middleware.Idempotency, chainH.TransferAsset)
|
||||
ch.Post("/market/list", chainUserLimiter, middleware.Idempotency, chainH.ListOnMarket)
|
||||
ch.Post("/market/buy", chainUserLimiter, middleware.Idempotency, chainH.BuyFromMarket)
|
||||
ch.Post("/market/cancel", chainUserLimiter, middleware.Idempotency, chainH.CancelListing)
|
||||
ch.Post("/inventory/equip", chainUserLimiter, middleware.Idempotency, chainH.EquipItem)
|
||||
ch.Post("/inventory/unequip", chainUserLimiter, middleware.Idempotency, chainH.UnequipItem)
|
||||
ch.Post("/transfer", chainUserLimiter, middleware.IdempotencyRequired, chainH.Transfer)
|
||||
ch.Post("/asset/transfer", chainUserLimiter, middleware.IdempotencyRequired, chainH.TransferAsset)
|
||||
ch.Post("/market/list", chainUserLimiter, middleware.IdempotencyRequired, chainH.ListOnMarket)
|
||||
ch.Post("/market/buy", chainUserLimiter, middleware.IdempotencyRequired, chainH.BuyFromMarket)
|
||||
ch.Post("/market/cancel", chainUserLimiter, middleware.IdempotencyRequired, chainH.CancelListing)
|
||||
ch.Post("/inventory/equip", chainUserLimiter, middleware.IdempotencyRequired, chainH.EquipItem)
|
||||
ch.Post("/inventory/unequip", chainUserLimiter, middleware.IdempotencyRequired, chainH.UnequipItem)
|
||||
|
||||
// Chain - Admin Transactions (admin only, idempotency-protected)
|
||||
chainAdmin := api.Group("/chain/admin", middleware.Auth, middleware.AdminOnly)
|
||||
chainAdmin.Post("/mint", middleware.Idempotency, chainH.MintAsset)
|
||||
chainAdmin.Post("/reward", middleware.Idempotency, chainH.GrantReward)
|
||||
chainAdmin.Post("/template", middleware.Idempotency, chainH.RegisterTemplate)
|
||||
chainAdmin.Post("/mint", middleware.IdempotencyRequired, chainH.MintAsset)
|
||||
chainAdmin.Post("/reward", middleware.IdempotencyRequired, chainH.GrantReward)
|
||||
chainAdmin.Post("/template", middleware.IdempotencyRequired, chainH.RegisterTemplate)
|
||||
|
||||
// Boss Raid - Client entry (JWT authenticated)
|
||||
bossRaid := api.Group("/bossraid", middleware.Auth)
|
||||
@@ -96,7 +100,7 @@ func Register(
|
||||
br := api.Group("/internal/bossraid", middleware.ServerAuth)
|
||||
br.Post("/entry", brH.RequestEntry)
|
||||
br.Post("/start", brH.StartRaid)
|
||||
br.Post("/complete", middleware.Idempotency, brH.CompleteRaid)
|
||||
br.Post("/complete", middleware.IdempotencyRequired, brH.CompleteRaid)
|
||||
br.Post("/fail", brH.FailRaid)
|
||||
br.Get("/room", brH.GetRoom)
|
||||
br.Post("/validate-entry", brH.ValidateEntryToken)
|
||||
@@ -106,6 +110,10 @@ func Register(
|
||||
p.Get("/profile", playerH.GetProfile)
|
||||
p.Put("/profile", playerH.UpdateProfile)
|
||||
|
||||
// Internal - Auth (API key auth)
|
||||
internalAuth := api.Group("/internal/auth", middleware.ServerAuth)
|
||||
internalAuth.Post("/verify", authH.VerifyToken)
|
||||
|
||||
// Internal - Player (API key auth)
|
||||
internalPlayer := api.Group("/internal/player", middleware.ServerAuth)
|
||||
internalPlayer.Get("/profile", playerH.InternalGetProfile)
|
||||
@@ -113,8 +121,8 @@ func Register(
|
||||
|
||||
// Internal - Game server endpoints (API key auth, username-based, idempotency-protected)
|
||||
internal := api.Group("/internal/chain", middleware.ServerAuth)
|
||||
internal.Post("/reward", middleware.Idempotency, chainH.InternalGrantReward)
|
||||
internal.Post("/mint", middleware.Idempotency, chainH.InternalMintAsset)
|
||||
internal.Post("/reward", middleware.IdempotencyRequired, chainH.InternalGrantReward)
|
||||
internal.Post("/mint", middleware.IdempotencyRequired, chainH.InternalMintAsset)
|
||||
internal.Get("/balance", chainH.InternalGetBalance)
|
||||
internal.Get("/assets", chainH.InternalGetAssets)
|
||||
internal.Get("/inventory", chainH.InternalGetInventory)
|
||||
|
||||
Reference in New Issue
Block a user