- 입력 검증 강화 (로그인/체인 핸들러 전체) - boss raid 비관적 잠금으로 동시성 문제 해결 - SSAFY 사용자명 sanitize + 트랜잭션 처리 - constant-time API 키 비교, 보안 헤더, graceful shutdown - 안전하지 않은 기본값 경고 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
)
|
||||
|
||||
const maxLimit = 200
|
||||
const maxIDLength = 256 // max length for string IDs (assetId, listingId, etc.)
|
||||
|
||||
type Handler struct {
|
||||
svc *Service
|
||||
@@ -40,6 +41,10 @@ func parsePagination(c *fiber.Ctx) (int, int) {
|
||||
return offset, limit
|
||||
}
|
||||
|
||||
func validID(s string) bool {
|
||||
return s != "" && len(s) <= maxIDLength
|
||||
}
|
||||
|
||||
func chainError(c *fiber.Ctx, status int, userMsg string, err error) error {
|
||||
log.Printf("chain error: %s: %v", userMsg, err)
|
||||
return c.Status(status).JSON(fiber.Map{"error": userMsg})
|
||||
@@ -90,8 +95,8 @@ func (h *Handler) GetAssets(c *fiber.Ctx) error {
|
||||
|
||||
func (h *Handler) GetAsset(c *fiber.Ctx) error {
|
||||
assetID := c.Params("id")
|
||||
if assetID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "asset id is required"})
|
||||
if !validID(assetID) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "유효한 asset id가 필요합니다"})
|
||||
}
|
||||
result, err := h.svc.GetAsset(assetID)
|
||||
if err != nil {
|
||||
@@ -126,8 +131,8 @@ func (h *Handler) GetMarketListings(c *fiber.Ctx) error {
|
||||
|
||||
func (h *Handler) GetMarketListing(c *fiber.Ctx) error {
|
||||
listingID := c.Params("id")
|
||||
if listingID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "listing id is required"})
|
||||
if !validID(listingID) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "유효한 listing id가 필요합니다"})
|
||||
}
|
||||
result, err := h.svc.GetListing(listingID)
|
||||
if err != nil {
|
||||
@@ -151,7 +156,7 @@ func (h *Handler) Transfer(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.To == "" || req.Amount == 0 {
|
||||
if !validID(req.To) || req.Amount == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "to와 amount는 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.Transfer(userID, req.To, req.Amount)
|
||||
@@ -173,7 +178,7 @@ func (h *Handler) TransferAsset(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.AssetID == "" || req.To == "" {
|
||||
if !validID(req.AssetID) || !validID(req.To) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId와 to는 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.TransferAsset(userID, req.AssetID, req.To)
|
||||
@@ -195,7 +200,7 @@ func (h *Handler) ListOnMarket(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.AssetID == "" || req.Price == 0 {
|
||||
if !validID(req.AssetID) || req.Price == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId와 price는 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.ListOnMarket(userID, req.AssetID, req.Price)
|
||||
@@ -216,7 +221,7 @@ func (h *Handler) BuyFromMarket(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.ListingID == "" {
|
||||
if !validID(req.ListingID) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "listingId는 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.BuyFromMarket(userID, req.ListingID)
|
||||
@@ -237,7 +242,7 @@ func (h *Handler) CancelListing(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.ListingID == "" {
|
||||
if !validID(req.ListingID) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "listingId는 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.CancelListing(userID, req.ListingID)
|
||||
@@ -259,7 +264,7 @@ func (h *Handler) EquipItem(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.AssetID == "" || req.Slot == "" {
|
||||
if !validID(req.AssetID) || !validID(req.Slot) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId와 slot은 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.EquipItem(userID, req.AssetID, req.Slot)
|
||||
@@ -280,7 +285,7 @@ func (h *Handler) UnequipItem(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.AssetID == "" {
|
||||
if !validID(req.AssetID) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId는 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.UnequipItem(userID, req.AssetID)
|
||||
@@ -301,7 +306,7 @@ func (h *Handler) MintAsset(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.TemplateID == "" || req.OwnerPubKey == "" {
|
||||
if !validID(req.TemplateID) || !validID(req.OwnerPubKey) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "templateId와 ownerPubKey는 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.MintAsset(req.TemplateID, req.OwnerPubKey, req.Properties)
|
||||
@@ -320,7 +325,7 @@ func (h *Handler) GrantReward(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.RecipientPubKey == "" {
|
||||
if !validID(req.RecipientPubKey) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "recipientPubKey는 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.GrantReward(req.RecipientPubKey, req.TokenAmount, req.Assets)
|
||||
@@ -340,7 +345,7 @@ func (h *Handler) RegisterTemplate(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.ID == "" || req.Name == "" {
|
||||
if !validID(req.ID) || !validID(req.Name) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id와 name은 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.RegisterTemplate(req.ID, req.Name, req.Schema, req.Tradeable)
|
||||
@@ -362,7 +367,7 @@ func (h *Handler) InternalGrantReward(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.Username == "" {
|
||||
if !validID(req.Username) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.GrantRewardByUsername(req.Username, req.TokenAmount, req.Assets)
|
||||
@@ -382,7 +387,7 @@ func (h *Handler) InternalMintAsset(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.TemplateID == "" || req.Username == "" {
|
||||
if !validID(req.TemplateID) || !validID(req.Username) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "templateId와 username은 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.MintAssetByUsername(req.TemplateID, req.Username, req.Properties)
|
||||
@@ -395,7 +400,7 @@ func (h *Handler) InternalMintAsset(c *fiber.Ctx) error {
|
||||
// InternalGetBalance returns balance by username. For game server use.
|
||||
func (h *Handler) InternalGetBalance(c *fiber.Ctx) error {
|
||||
username := c.Query("username")
|
||||
if username == "" {
|
||||
if !validID(username) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.GetBalanceByUsername(username)
|
||||
@@ -408,7 +413,7 @@ func (h *Handler) InternalGetBalance(c *fiber.Ctx) error {
|
||||
// InternalGetAssets returns assets by username. For game server use.
|
||||
func (h *Handler) InternalGetAssets(c *fiber.Ctx) error {
|
||||
username := c.Query("username")
|
||||
if username == "" {
|
||||
if !validID(username) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"})
|
||||
}
|
||||
offset, limit := parsePagination(c)
|
||||
@@ -423,7 +428,7 @@ func (h *Handler) InternalGetAssets(c *fiber.Ctx) error {
|
||||
// InternalGetInventory returns inventory by username. For game server use.
|
||||
func (h *Handler) InternalGetInventory(c *fiber.Ctx) error {
|
||||
username := c.Query("username")
|
||||
if username == "" {
|
||||
if !validID(username) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"})
|
||||
}
|
||||
result, err := h.svc.GetInventoryByUsername(username)
|
||||
|
||||
Reference in New Issue
Block a user