Skip to content

Add API key authenticated REST API to back office - #191

Merged
Atralupus merged 2 commits into
planetarium:mainfrom
Atralupus:backoffice-api-key
Aug 31, 2026
Merged

Add API key authenticated REST API to back office#191
Atralupus merged 2 commits into
planetarium:mainfrom
Atralupus:backoffice-api-key

Conversation

@Atralupus

@Atralupus Atralupus commented Aug 31, 2026

Copy link
Copy Markdown
Member

목적

백오피스 기능이 Blazor UI(Google OAuth)에서만 사용 가능해서 외부 연동·자동화가 불가능했습니다. 기존 페이지의 동작을 그대로 REST 엔드포인트로 노출해 API Key 로 호출할 수 있게 합니다.

NineChronicles.Backoffice 가 같은 방식(X-API-Key + NC_API_KEY)으로 API 를 노출한 것과 동일한 패턴입니다.

인증

  • Authentication/ApiKeyAuthenticationHandler.csX-API-Key 헤더를 ARENA_API_KEY 설정값과 비교
  • 컨트롤러에만 [Authorize(AuthenticationSchemes = "ApiKey")] 적용. Blazor UI 는 기존 Google OAuth 그대로
  • 키 없음 / 빈 값 / 불일치 모두 401. ARENA_API_KEY 미설정 시에도 401(에러 로그 남김)

엔드포인트 (24개)

기존 페이지와 1:1 대응합니다.

Controller Route 대응 페이지
SeasonsController api/seasons — 목록/조회/생성/수정/종료블록 조정/삭제/다음 시작블록 ManageSeasons
PoliciesController api/policies/battle, api/policies/refresh — 조회/추가 Policy
ConfirmTxController api/confirm-tx/battles, /ticket-purchases + 리뷰 처리 ConfirmTx
RankingCacheController api/ranking-cache 상태/시즌 준비/다음 라운드 준비/캐시 초기화 RankingCache, CacheInitialization
LeaderboardController api/leaderboard/seasons, /{id}, /{id}/csv, /{id}/staking-csv Leaderboard
BlockTrackerController api/block-tracker BlockTrackerStatus

설계 메모

  • 엔티티를 직접 직렬화하지 않습니다. SeasonRound 가 서로를 참조해 순환이 생기므로 Models/ApiModels.cs 의 record 로 투영합니다.
  • 응답은 공통 래퍼 ApiResponse<T> { success, message, data } 를 사용합니다.
  • Swagger 에 ApiKey 보안 정의를 등록했고, XML 주석을 문서에 포함합니다 (GenerateDocumentationFile).
  • 검증 로직(티켓 가격 개수 일치 등)은 UI 와 동일하게 유지했습니다.

UI 동작을 그대로 옮긴 부분 (의도적)

리뷰 시 확인이 필요할 수 있어 남깁니다. 둘 다 기존 UI 동작을 바꾸지 않으려고 그대로 옮겼고, 코드에 주석을 달아뒀습니다.

  1. POST api/ranking-cache/initialize 는 캐시된 라운드의 IdICacheInitializationService.InitializeRankingCacheAsync 의 두 번째 인자로 넘깁니다. 인터페이스 파라미터명은 roundIndex 입니다 (CacheInitialization.razor 와 동일).
  2. POST api/confirm-tx/ticket-purchases/{id}/review 는 battle ticket purchase log 만 갱신합니다 (ConfirmTx.razor 와 동일). refresh ticket purchase id 로는 갱신되지 않습니다.

배포

ARENA_API_KEY 환경변수 주입이 필요합니다. 미설정 상태로 배포되면 API 는 전부 401 이 되고 기존 Blazor UI 는 영향받지 않습니다.

검증

  • dotnet build ArenaService.BackOffice/ArenaService.BackOffice.csproj — 성공, 신규 코드에서 경고 없음
  • 로컬 실행 후 확인
    • /swagger/v1/swagger.json 200, 24개 엔드포인트 노출
    • 키 없음 → 401, 잘못된 키 → 401, 올바른 키 → 인증 통과
    • / (Blazor UI) → 302 Google 로그인 (기존과 동일)

🤖 Generated with Claude Code

https://claude.ai/code/session_01LsR64huwuS3v6GB8RejLCy


#189 과의 관계 (2번째 커밋에서 반영)

같은 목적의 선행 PR #189 (Add admin REST API endpoints for backoffice operations) 가 열려 있습니다. 확인 후 접근이 달라 이 PR 을 유지하되, #189 이 이미 잡아둔 지적사항을 흡수했습니다.

#189 이 PR
위치 ArenaService (게임 API 본체) ArenaService.BackOffice
인증 [Authorize(Roles = "Admin")] — ES256K JWT + ALLOWED_ADMIN_PUBLIC_KEY X-API-Key + ARENA_API_KEY
범위 20 endpoints 24 endpoints (스테이킹 CSV, 랭킹 캐시 상태, block tracker, 시즌 페이지네이션, next-start-block 추가)

두 PR 은 변경 파일이 겹치지 않아 충돌 없이 공존 가능합니다. 어느 쪽을 살릴지는 리뷰에서 정해주시면 따르겠습니다.

#189 에서 가져온 수정

  1. SeasonRepository.AddSeasonWithRoundsAsyncendBlock 계산이 int 곱셈이라 오버플로 가능 → checked + long 연산
  2. 티켓 구매 리뷰가 battle 로그만 갱신하던 것을 type 파라미터(case-insensitive)로 battle/refresh 모두 지원. UpdateRefreshTicketPurchaseLog 가 이미 있는데 UI 제약을 그대로 옮겼던 부분입니다
  3. 미검토 battle/refresh 조회를 Task.WhenAll 로 병렬화
  4. 주소를 소문자로 통일 — 기존 API 응답 및 리더보드 CSV 관례와 일치

단위 테스트 (44개)

ArenaService.BackOffice.Tests 프로젝트를 새로 추가했습니다. ArenaService.Tests 에 넣지 않은 이유는 두 웹 프로젝트가 각각 ArenaService.Client 타입을 생성해서, 양쪽을 참조하면 IHeadlessClientCS0433 로 모호해지기 때문입니다.

  • ApiKeyAuthenticationHandlerTests — 키 일치/불일치/공백/헤더 없음/대소문자 차이/미설정 (7)
  • SeasonsControllerTests — 페이징, deletable 판정, 캐시 장애 시 null, 블록범위 겹침 409, 오버플로 방지, 서버사이드 block index 삭제 검증 (13)
  • PoliciesControllerTests — 가격 개수 검증 실패/성공 (6)
  • ConfirmTxControllerTests — battle/refresh 분기, 대소문자 무시, 알 수 없는 type 400 (8)
  • RankingCacheControllerTests — 상태 조회, 첫 라운드 경계, 라운드 없는 시즌 400, 초기화 실패 500 (7)
  • LeaderboardControllerTests — 주소 소문자, 랭크/전적 투영, CSV 파일명 (3)

검증: dotnet test ArenaService.BackOffice.Tests 44개 통과, dotnet test ArenaService.Tests 기존 39개 통과 (회귀 없음).

머지 전 필요한 조치

.github/workflows/test.yaml 의 CI matrix 에 새 테스트 프로젝트를 추가해야 합니다. 봇 토큰에 workflow 스코프가 없어 이 PR 에 포함하지 못했습니다.

        project: [ArenaService.Tests, ArenaService.BackOffice.Tests]

이 줄을 고치지 않으면 새 테스트 44개가 CI 에서 실행되지 않습니다.

Atralupus and others added 2 commits August 31, 2026 16:59
Expose the back office operations as REST controllers so they can be
driven without the Blazor UI.

Authentication reads the X-API-Key header and compares it against the
ARENA_API_KEY configuration value. The Blazor UI keeps using Google
OAuth; only the controllers opt into the new scheme.

Controllers mirror the existing pages one to one: seasons (ManageSeasons),
ticket policies (Policy), transaction review (ConfirmTx), cache
preparation (RankingCache, CacheInitialization), leaderboards and
settlement CSVs (Leaderboard), and battle tx tracker progress
(BlockTrackerStatus).

Season and Round reference each other, so responses project into records
instead of serializing entities. Every response uses a common
ApiResponse envelope, and Swagger documents the endpoints with the API
key security definition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsR64huwuS3v6GB8RejLCy
Apply the fixes planetarium#189 already landed on its own admin API, and cover the
back office controllers with tests.

- AddSeasonWithRoundsAsync computed endBlock with an int multiplication
  that could overflow before widening to long. Compute it checked, on
  long.
- Ticket purchase review now takes a type parameter and updates refresh
  ticket purchase logs too. It previously only updated battle ticket
  logs, mirroring a limitation of the Blazor page even though
  UpdateRefreshTicketPurchaseLog exists.
- Unreviewed battle and refresh purchases are now queried concurrently.
- Addresses are lower cased, matching the existing API responses and the
  leaderboard CSV.

Tests live in a new ArenaService.BackOffice.Tests project rather than
ArenaService.Tests: both web projects generate ArenaService.Client types,
so a single test project referencing both cannot resolve IHeadlessClient.
The project is added to the solution.

The CI matrix in .github/workflows/test.yaml still lists only
ArenaService.Tests, so the new project is not run yet. Adding it needs a
token with the workflow scope; see the pull request description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsR64huwuS3v6GB8RejLCy
@Atralupus
Atralupus merged commit 74e1894 into planetarium:main Aug 31, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant