feat: 인프라 개선 — 헬스체크, 로깅, 보안, CI 검증
- /health + /ready 엔드포인트 추가 (DB/Redis 상태 확인) - RequestID 미들웨어 + 구조화 JSON 로깅 - 체인 트랜잭션 per-user rate limit (20 req/min) - DB 커넥션 풀 설정 (MaxOpen 25, MaxIdle 10, MaxLifetime 5m) - Graceful Shutdown 시 Redis/MySQL 연결 정리 - Dockerfile HEALTHCHECK 추가 - CI에 go vet + 빌드 검증 단계 추가 (deploy 전 실행) - 보스 레이드 클라이언트 입장 API (JWT 인증) - Player 프로필 모듈 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -129,6 +129,114 @@ func (h *Handler) FailRaid(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// RequestEntryAuth handles POST /api/bossraid/entry (JWT authenticated).
|
||||
// Called by the game client to request boss raid entry.
|
||||
// The authenticated user must be included in the usernames list.
|
||||
func (h *Handler) RequestEntryAuth(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
Usernames []string `json:"usernames"`
|
||||
BossID int `json:"bossId"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
|
||||
// 인증된 유저의 username
|
||||
authUsername, _ := c.Locals("username").(string)
|
||||
if authUsername == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "인증 정보가 없습니다"})
|
||||
}
|
||||
|
||||
// 빈 usernames이면 솔로 입장 — 본인만 포함
|
||||
if len(req.Usernames) == 0 {
|
||||
req.Usernames = []string{authUsername}
|
||||
}
|
||||
if req.BossID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bossId는 필수입니다"})
|
||||
}
|
||||
|
||||
// 인증된 유저가 요청 목록에 포함되어 있는지 검증
|
||||
found := false
|
||||
for _, u := range req.Usernames {
|
||||
if u == authUsername {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "본인이 입장 목록에 포함되어야 합니다"})
|
||||
}
|
||||
|
||||
for _, u := range req.Usernames {
|
||||
if len(u) == 0 || len(u) > 50 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "유효하지 않은 username입니다"})
|
||||
}
|
||||
}
|
||||
|
||||
room, tokens, err := h.svc.RequestEntryWithTokens(req.Usernames, req.BossID)
|
||||
if err != nil {
|
||||
return bossError(c, fiber.StatusConflict, err.Error(), err)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||
"roomId": room.ID,
|
||||
"sessionName": room.SessionName,
|
||||
"bossId": room.BossID,
|
||||
"players": req.Usernames,
|
||||
"status": room.Status,
|
||||
"entryToken": tokens[authUsername],
|
||||
})
|
||||
}
|
||||
|
||||
// GetMyEntryToken handles GET /api/bossraid/my-entry-token (JWT authenticated).
|
||||
// Returns the pending entry token for the authenticated user.
|
||||
// Called by party members after the leader requests entry.
|
||||
func (h *Handler) GetMyEntryToken(c *fiber.Ctx) error {
|
||||
username, _ := c.Locals("username").(string)
|
||||
if username == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "인증 정보가 없습니다"})
|
||||
}
|
||||
|
||||
sessionName, entryToken, err := h.svc.GetMyEntryToken(username)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"sessionName": sessionName,
|
||||
"entryToken": entryToken,
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateEntryToken handles POST /api/internal/bossraid/validate-entry (ServerAuth).
|
||||
// Called by the dedicated server to validate a player's entry token.
|
||||
// Consumes the token (one-time use).
|
||||
func (h *Handler) ValidateEntryToken(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
EntryToken string `json:"entryToken"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
}
|
||||
if req.EntryToken == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "entryToken은 필수입니다"})
|
||||
}
|
||||
|
||||
username, sessionName, err := h.svc.ValidateEntryToken(req.EntryToken)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"valid": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"valid": true,
|
||||
"username": username,
|
||||
"sessionName": sessionName,
|
||||
})
|
||||
}
|
||||
|
||||
// GetRoom handles GET /api/internal/bossraid/room
|
||||
// Query param: sessionName
|
||||
func (h *Handler) GetRoom(c *fiber.Ctx) error {
|
||||
|
||||
Reference in New Issue
Block a user