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:
94
main.go
94
main.go
@@ -4,6 +4,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"a301_server/internal/bossraid"
|
||||
"a301_server/internal/chain"
|
||||
"a301_server/internal/download"
|
||||
"a301_server/internal/player"
|
||||
|
||||
"github.com/tolelom/tolchain/core"
|
||||
"a301_server/pkg/config"
|
||||
@@ -35,7 +37,7 @@ func main() {
|
||||
log.Println("MySQL 연결 성공")
|
||||
|
||||
// AutoMigrate
|
||||
if err := database.DB.AutoMigrate(&auth.User{}, &announcement.Announcement{}, &download.Info{}, &chain.UserWallet{}, &bossraid.BossRoom{}); err != nil {
|
||||
if err := database.DB.AutoMigrate(&auth.User{}, &announcement.Announcement{}, &download.Info{}, &chain.UserWallet{}, &bossraid.BossRoom{}, &player.PlayerProfile{}); err != nil {
|
||||
log.Fatalf("AutoMigrate 실패: %v", err)
|
||||
}
|
||||
|
||||
@@ -49,13 +51,6 @@ func main() {
|
||||
authSvc := auth.NewService(authRepo, database.RDB)
|
||||
authHandler := auth.NewHandler(authSvc)
|
||||
|
||||
// 초기 admin 계정 생성
|
||||
if err := authSvc.EnsureAdmin(config.C.AdminUsername, config.C.AdminPassword); err != nil {
|
||||
log.Printf("admin 계정 생성 실패: %v", err)
|
||||
} else {
|
||||
log.Printf("admin 계정 확인 완료: %s", config.C.AdminUsername)
|
||||
}
|
||||
|
||||
// Chain (blockchain integration)
|
||||
chainClient := chain.NewClient(config.C.ChainNodeURL)
|
||||
chainRepo := chain.NewRepository(database.DB)
|
||||
@@ -80,9 +75,33 @@ func main() {
|
||||
return err
|
||||
})
|
||||
|
||||
// Player Profile
|
||||
playerRepo := player.NewRepository(database.DB)
|
||||
playerSvc := player.NewService(playerRepo)
|
||||
playerSvc.SetUserResolver(func(username string) (uint, error) {
|
||||
user, err := authRepo.FindByUsername(username)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return user.ID, nil
|
||||
})
|
||||
playerHandler := player.NewHandler(playerSvc)
|
||||
|
||||
// 회원가입 시 플레이어 프로필 자동 생성
|
||||
authSvc.SetProfileCreator(func(userID uint) error {
|
||||
return playerSvc.CreateProfile(userID)
|
||||
})
|
||||
|
||||
// 초기 admin 계정 생성 (콜백 등록 후 실행)
|
||||
if err := authSvc.EnsureAdmin(config.C.AdminUsername, config.C.AdminPassword); err != nil {
|
||||
log.Printf("admin 계정 생성 실패: %v", err)
|
||||
} else {
|
||||
log.Printf("admin 계정 확인 완료: %s", config.C.AdminUsername)
|
||||
}
|
||||
|
||||
// Boss Raid
|
||||
brRepo := bossraid.NewRepository(database.DB)
|
||||
brSvc := bossraid.NewService(brRepo)
|
||||
brSvc := bossraid.NewService(brRepo, database.RDB)
|
||||
brSvc.SetRewardGranter(func(username string, tokenAmount uint64, assets []core.MintAssetPayload) error {
|
||||
_, err := chainSvc.GrantRewardByUsername(username, tokenAmount, assets)
|
||||
return err
|
||||
@@ -105,7 +124,11 @@ func main() {
|
||||
StreamRequestBody: true,
|
||||
BodyLimit: 4 * 1024 * 1024 * 1024, // 4GB
|
||||
})
|
||||
app.Use(logger.New())
|
||||
app.Use(middleware.RequestID)
|
||||
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: "https://a301.tolelom.xyz",
|
||||
@@ -137,7 +160,40 @@ func main() {
|
||||
},
|
||||
})
|
||||
|
||||
routes.Register(app, authHandler, annHandler, dlHandler, chainHandler, brHandler, authLimiter, apiLimiter)
|
||||
// Health check handlers
|
||||
healthCheck := func(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
readyCheck := func(c *fiber.Ctx) error {
|
||||
sqlDB, err := database.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 := database.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"})
|
||||
}
|
||||
|
||||
// Rate limiting: 체인 트랜잭션 (유저별 분당 20회)
|
||||
chainUserLimiter := 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 c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{"error": "트랜잭션 요청이 너무 많습니다. 잠시 후 다시 시도해주세요"})
|
||||
},
|
||||
})
|
||||
|
||||
routes.Register(app, authHandler, annHandler, dlHandler, chainHandler, brHandler, playerHandler, authLimiter, apiLimiter, healthCheck, readyCheck, chainUserLimiter)
|
||||
|
||||
// Graceful shutdown
|
||||
go func() {
|
||||
@@ -148,6 +204,22 @@ func main() {
|
||||
if err := app.ShutdownWithTimeout(10 * time.Second); err != nil {
|
||||
log.Printf("서버 종료 실패: %v", err)
|
||||
}
|
||||
// Redis 연결 정리
|
||||
if database.RDB != nil {
|
||||
if err := database.RDB.Close(); err != nil {
|
||||
log.Printf("Redis 종료 실패: %v", err)
|
||||
} else {
|
||||
log.Println("Redis 연결 종료 완료")
|
||||
}
|
||||
}
|
||||
// MySQL 연결 정리
|
||||
if sqlDB, err := database.DB.DB(); err == nil {
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
log.Printf("MySQL 종료 실패: %v", err)
|
||||
} else {
|
||||
log.Println("MySQL 연결 종료 완료")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Fatal(app.Listen(":" + config.C.AppPort))
|
||||
|
||||
Reference in New Issue
Block a user