Files
a301_server/internal/bossraid/handler.go
tolelom 423e2832a0
Some checks failed
Server CI/CD / lint-and-build (push) Failing after 30s
Server CI/CD / deploy (push) Has been skipped
fix: 3차 리뷰 LOW — 에러 메시지 일관성, Redis 타임아웃, 입력 검증
- 5개 핸들러 err.Error() → 제네릭 메시지 (Login, Refresh, SSAFY, Ticket, BossRaid)
- Redis context.Background() → WithTimeout 5s (10곳)
- SprintMultiplier 범위 검증 추가
- 방어적 문서화 (SSAFY 충돌, zip bomb, body limit prefix, 로그 주입)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 19:05:17 +09:00

255 lines
7.8 KiB
Go

package bossraid
import (
"log"
"github.com/gofiber/fiber/v2"
)
type Handler struct {
svc *Service
}
func NewHandler(svc *Service) *Handler {
return &Handler{svc: svc}
}
func bossError(c *fiber.Ctx, status int, userMsg string, err error) error {
log.Printf("bossraid error: %s: %v", userMsg, err)
return c.Status(status).JSON(fiber.Map{"error": userMsg})
}
// RequestEntry handles POST /api/internal/bossraid/entry
// Called by MMO server when a party requests boss raid entry.
func (h *Handler) RequestEntry(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": "잘못된 요청입니다"})
}
if len(req.Usernames) == 0 || req.BossID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "usernames와 bossId는 필수입니다"})
}
for _, u := range req.Usernames {
if len(u) == 0 || len(u) > 50 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "유효하지 않은 username입니다"})
}
}
room, err := h.svc.RequestEntry(req.Usernames, req.BossID)
if err != nil {
return bossError(c, fiber.StatusConflict, "보스 레이드 입장에 실패했습니다", err)
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"roomId": room.ID,
"sessionName": room.SessionName,
"bossId": room.BossID,
"players": req.Usernames,
"status": room.Status,
})
}
// StartRaid handles POST /api/internal/bossraid/start
// Called by dedicated server when the Fusion session begins.
func (h *Handler) StartRaid(c *fiber.Ctx) error {
var req struct {
SessionName string `json:"sessionName"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if req.SessionName == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "sessionName은 필수입니다"})
}
room, err := h.svc.StartRaid(req.SessionName)
if err != nil {
return bossError(c, fiber.StatusBadRequest, "레이드 시작에 실패했습니다", err)
}
return c.JSON(fiber.Map{
"roomId": room.ID,
"sessionName": room.SessionName,
"status": room.Status,
})
}
// CompleteRaid handles POST /api/internal/bossraid/complete
// Called by dedicated server when the boss is killed. Distributes rewards.
func (h *Handler) CompleteRaid(c *fiber.Ctx) error {
var req struct {
SessionName string `json:"sessionName"`
Rewards []PlayerReward `json:"rewards"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if req.SessionName == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "sessionName은 필수입니다"})
}
room, results, err := h.svc.CompleteRaid(req.SessionName, req.Rewards)
if err != nil {
return bossError(c, fiber.StatusBadRequest, "레이드 완료 처리에 실패했습니다", err)
}
return c.JSON(fiber.Map{
"roomId": room.ID,
"sessionName": room.SessionName,
"status": room.Status,
"rewardResults": results,
})
}
// FailRaid handles POST /api/internal/bossraid/fail
// Called by dedicated server on timeout or party wipe.
func (h *Handler) FailRaid(c *fiber.Ctx) error {
var req struct {
SessionName string `json:"sessionName"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if req.SessionName == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "sessionName은 필수입니다"})
}
room, err := h.svc.FailRaid(req.SessionName)
if err != nil {
return bossError(c, fiber.StatusBadRequest, "레이드 실패 처리에 실패했습니다", err)
}
return c.JSON(fiber.Map{
"roomId": room.ID,
"sessionName": room.SessionName,
"status": room.Status,
})
}
// 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)
}
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 {
sessionName := c.Query("sessionName")
if sessionName == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "sessionName은 필수입니다"})
}
room, err := h.svc.GetRoom(sessionName)
if err != nil {
return bossError(c, fiber.StatusNotFound, "방을 찾을 수 없습니다", err)
}
return c.JSON(room)
}