Skip to content

[Feature] 관리자 기능 추가 및 보안 개선#66

Open
kimjuneon wants to merge 5 commits into
developfrom
feature/admin-membership-security
Open

[Feature] 관리자 기능 추가 및 보안 개선#66
kimjuneon wants to merge 5 commits into
developfrom
feature/admin-membership-security

Conversation

@kimjuneon
Copy link
Copy Markdown
Contributor

@kimjuneon kimjuneon commented May 22, 2026

변경 사항

  • 관리자 멤버십 활성화 API와 화면 기능 추가
  • 임시 사용자 지갑/멤버십 API 제거
  • 관리자 화면 XSS 방어와 관리자 JWT secret 설정 강화

Summary by CodeRabbit

  • New Features

    • 관리자가 사용자 멤버십을 활성화할 수 있는 API 및 관리자 UI(사유 입력·활성화 버튼) 추가
  • Bug Fixes

    • 사용자 상세의 계정 상태 배지 렌더링 수정(HTML 이스케이프 처리)
    • 클라이언트 IP 판단 로직 개선(신뢰 프록시 설정 반영)
  • Refactor

    • 임시로 제공되던 멤버십/송금용 엔드포인트 제거
  • Chores

    • 관리자용 JWT/감사 관련 설정 및 감사 액션 라벨 추가

Review Change Stack

@kimjuneon kimjuneon requested a review from Sehi55 May 22, 2026 08:48
@kimjuneon kimjuneon self-assigned this May 22, 2026
@kimjuneon kimjuneon added the enhancement New feature or request label May 22, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

Warning

Review limit reached

@kimjuneon, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 1 review/hour. Refill in 6 minutes and 59 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0d9900d-e5aa-4b11-b893-bc7db3d4ddfd

📥 Commits

Reviewing files that changed from the base of the PR and between 85c91cb and 0379036.

📒 Files selected for processing (1)
  • manabom/src/main/java/mannabom_server/manabom/presentation/admin/controller/AdminWalletController.java

Walkthrough

관리자 UI에서 멤버십 활성화 사유를 제출하면 컨트롤러가 IP/권한을 검증해 서비스의 activateMembership을 호출하고, 런타임 정책을 반영해 지갑 멤버십을 활성화하고 감사 로그와 확장된 지갑 응답을 반환합니다.

변경사항

관리자 멤버십 활성화 기능

Layer / File(s) Summary
요청/응답 DTO 계약
manabom/src/main/java/.../AdminActivateMembershipRequest.java, manabom/src/main/java/.../AdminWalletResponse.java, manabom/src/main/java/.../AdminAuditActionType.java
AdminActivateMembershipRequest는 필수 reason 필드를 검증하고, AdminWalletResponsemembershipActive, membershipCycleStartAt, membershipActiveUntil을 추가합니다. MEMBERSHIP_ACTIVATE 감사 액션이 정의됩니다.
서비스 멤버십 활성화 로직
manabom/src/main/java/.../AdminWalletService.java
activateMembership 트랜잭션이 권한 검증, 사용자/지갑 조회(또는 생성), runtime policy 조회에 따른 wallet.activateMembership 호출, 감사 로그 기록, 확장된 응답 반환을 수행하도록 구현되었습니다. toResponse가 멤버십 필드를 포함하도록 확장되었습니다.
컨트롤러 엔드포인트 및 IP 처리
manabom/src/main/java/.../AdminWalletController.java
POST /api/admin/users/{userId}/wallet/membership 핸들러가 추가되고, trustProxy/trustedProxies 설정에 따라 X-Forwarded-For 사용 여부를 판단해 클라이언트 IP를 추출합니다.
관리자 JWT 설정 및 환경 구성
manabom/src/main/java/.../AdminJwtUtil.java, manabom/src/main/resources/application.yml, manabom/src/main/resources/application-prod.yml
app.admin.jwt 설정 섹션이 추가되어 관리자 전용 JWT secret(환경변수 바인딩)과 토큰 만료 시간을 정의하고, AdminJwtUtil의 기본값을 제거해 secret 설정을 필수화했습니다.
임시 기능 제거
manabom/src/main/java/.../UserInfoService.java, manabom/src/main/java/.../UserInfoController.java
임시용 activeMembershipaddTing 메서드/엔드포인트가 제거되어 공개 API에서 정리되었습니다.
관리자 UI 멤버십 활성화 기능
manabom/src/main/resources/static/admin/app.js
사용자 상세에 멤버십 활성화 패널과 activateMembership() 기능을 추가하고, 동시 클릭 방지/유효성 검사/요청 후 UI 초기화 및 목록 재로딩을 수행합니다. 상태 배지 렌더링은 kvHtml로 분리되고 감사 라벨에 MEMBERSHIP_ACTIVATE가 등록됩니다.

Sequence Diagram

sequenceDiagram
  participant AdminUI
  participant AdminWalletController
  participant AdminWalletService
  participant RuntimePolicyService
  participant TingWallet
  participant AuditLog
  AdminUI->>AdminWalletController: POST /users/{userId}/wallet/membership (reason)
  AdminWalletController->>AdminWalletService: activateMembership(admin, userId, request, ip)
  AdminWalletService->>RuntimePolicyService: snapshot().getBenefit().getMembership()
  AdminWalletService->>TingWallet: activateMembership(now, policy)
  AdminWalletService->>AuditLog: record(MEMBERSHIP_ACTIVATE, admin, userId, ip, reason)
  AdminWalletService->>AdminWalletController: AdminWalletResponse(with membership fields)
  AdminWalletController->>AdminUI: 200 OK (response)
Loading

예상 코드 리뷰 노력

🎯 4 (Complex) | ⏱️ ~45 minutes

시 🐰

멤버십 사유로 뾰족히 찡,
관리자 버튼 한 번에 반짝 활성,
정책 읽고 지갑에 불 붙이고,
감사 로그에 발자국 남기며,
UI와 설정까지 깔끔히 연결했네 🥕🐇

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 변경사항의 주요 내용을 명확히 반영하고 있으며, 관리자 기능 추가와 보안 개선이라는 두 가지 핵심 목표를 정확히 요약하고 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/admin-membership-security

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
manabom/src/main/java/mannabom_server/manabom/application/admin/dto/request/AdminActivateMembershipRequest.java (1)

10-11: ⚡ Quick win

reason 길이 상한을 DTO 계약에 같이 명시해주세요.

현재는 공백/빈값만 막고 있어 매우 긴 문자열이 그대로 감사 로그/저장 계층까지 전달됩니다. @Size(max = ...)를 추가해 입력 계약을 고정하는 편이 안전합니다.

제안 diff
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
@@
-    `@NotBlank`(message = "reason은 필수입니다.")
+    `@NotBlank`(message = "reason은 필수입니다.")
+    `@Size`(max = 255, message = "reason은 255자 이하여야 합니다.")
     private String reason;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@manabom/src/main/java/mannabom_server/manabom/application/admin/dto/request/AdminActivateMembershipRequest.java`
around lines 10 - 11, The DTO field 'reason' in AdminActivateMembershipRequest
currently only has `@NotBlank` allowing arbitrarily long input; add a length
constraint (e.g., annotate the field with `@Size`(max = 255) and a matching
message like "reason은 최대 255자입니다.") to fix the DTO contract, import
javax.validation.constraints.Size, and adjust any tests or validation messages
that assert on this DTO to reflect the new max length.
manabom/src/main/resources/static/admin/app.js (1)

832-834: ⚡ Quick win

kvHtml의 신뢰 경계를 함수 계약으로 명확히 해주세요.

valueHtml을 그대로 주입하는 구조라 향후 호출부에서 사용자 입력이 들어오면 XSS 회귀 지점이 됩니다. 함수명을 kvTrustedHtml처럼 의도를 드러내거나, 주석으로 “escape 보장된 HTML만 허용” 계약을 명시하는 쪽이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@manabom/src/main/resources/static/admin/app.js` around lines 832 - 834, The
kvHtml function currently injects valueHtml without escaping, creating a future
XSS regression; either change the function name to kvTrustedHtml to signal it
requires already-escaped/trusted HTML or update kvHtml to accept untrusted input
and escape it (use escapeHtml) before concatenation, and add a short JSDoc
comment above the function documenting the contract (e.g., "`@param` {string}
valueHtml - must be trusted HTML" if renaming, or "`@param` {string} value -
untrusted text will be escaped" if changing behavior) so callers know the safety
expectations; reference kvHtml (or kvTrustedHtml if renaming) when making the
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@manabom/src/main/java/mannabom_server/manabom/presentation/admin/controller/AdminWalletController.java`:
- Around line 40-48: The activateMembership endpoint in AdminWalletController
currently passes a clientIp derived from request headers that can be spoofed;
update the clientIp(HttpServletRequest) logic (or the call site in
activateMembership) to default to request.getRemoteAddr() and only parse
X-Forwarded-For when a trusted-proxy flag or allowlist is configured and
validated; ensure you reject or ignore untrusted X-Forwarded-For values,
document/use a config flag (e.g., trustProxy) and check it before trusting
header-derived IPs so AdminWalletService.activateMembership receives a
non-spoofable audit IP.

In `@manabom/src/main/resources/static/admin/app.js`:
- Around line 425-443: The activateMembership function allows duplicate POSTs on
rapid clicks; add an in-flight guard or disable the membership button while the
request is pending and re-enable it in finally to prevent duplicate calls.
Before sending the POST in activateMembership, validate the reason from
getReasonValue("membershipReason") is non-empty (and show a client-side
error/return if empty), set a local flag like isActivatingMembership or disable
the UI control (e.g., the membership button referenced near
membershipReasonSelect/membershipReasonOther), perform the await request, and in
a finally block clear the flag or re-enable the button and still call
loadUserDetail/loadUsers on success; keep existing error handling via
showUserActionResult for failures.

---

Nitpick comments:
In
`@manabom/src/main/java/mannabom_server/manabom/application/admin/dto/request/AdminActivateMembershipRequest.java`:
- Around line 10-11: The DTO field 'reason' in AdminActivateMembershipRequest
currently only has `@NotBlank` allowing arbitrarily long input; add a length
constraint (e.g., annotate the field with `@Size`(max = 255) and a matching
message like "reason은 최대 255자입니다.") to fix the DTO contract, import
javax.validation.constraints.Size, and adjust any tests or validation messages
that assert on this DTO to reflect the new max length.

In `@manabom/src/main/resources/static/admin/app.js`:
- Around line 832-834: The kvHtml function currently injects valueHtml without
escaping, creating a future XSS regression; either change the function name to
kvTrustedHtml to signal it requires already-escaped/trusted HTML or update
kvHtml to accept untrusted input and escape it (use escapeHtml) before
concatenation, and add a short JSDoc comment above the function documenting the
contract (e.g., "`@param` {string} valueHtml - must be trusted HTML" if renaming,
or "`@param` {string} value - untrusted text will be escaped" if changing
behavior) so callers know the safety expectations; reference kvHtml (or
kvTrustedHtml if renaming) when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3c7e6fe2-a5e3-4b24-8d74-2017dff969d2

📥 Commits

Reviewing files that changed from the base of the PR and between bb07555 and 71dbd2a.

📒 Files selected for processing (11)
  • manabom/src/main/java/mannabom_server/manabom/application/admin/dto/request/AdminActivateMembershipRequest.java
  • manabom/src/main/java/mannabom_server/manabom/application/admin/dto/response/AdminWalletResponse.java
  • manabom/src/main/java/mannabom_server/manabom/application/admin/service/AdminWalletService.java
  • manabom/src/main/java/mannabom_server/manabom/application/userInfo/service/UserInfoService.java
  • manabom/src/main/java/mannabom_server/manabom/domain/admin/enums/AdminAuditActionType.java
  • manabom/src/main/java/mannabom_server/manabom/infrastructure/security/admin/AdminJwtUtil.java
  • manabom/src/main/java/mannabom_server/manabom/presentation/admin/controller/AdminWalletController.java
  • manabom/src/main/java/mannabom_server/manabom/presentation/userInfo/controller/UserInfoController.java
  • manabom/src/main/resources/application-prod.yml
  • manabom/src/main/resources/application.yml
  • manabom/src/main/resources/static/admin/app.js
💤 Files with no reviewable changes (2)
  • manabom/src/main/java/mannabom_server/manabom/presentation/userInfo/controller/UserInfoController.java
  • manabom/src/main/java/mannabom_server/manabom/application/userInfo/service/UserInfoService.java

Comment thread manabom/src/main/resources/static/admin/app.js
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
manabom/src/main/resources/static/admin/app.js (2)

343-347: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

권한 없는 관리자에게도 멤버십 활성화 UI가 노출됩니다.

백엔드는 AdminWalletService.activateMembership에서 SUPER_ADMIN/FINANCE만 허용하는데, 여기서는 모든 관리자에게 버튼이 보입니다. 권한이 없는 역할은 클릭 후 403만 보게 되니 state.admin.roles 기준으로 이 섹션을 숨기거나 비활성화해 주세요.

수정 예시
 function renderUserDetail(user) {
+    const canActivateMembership = (state.admin?.roles || []).some((role) =>
+        role === "SUPER_ADMIN" || role === "FINANCE"
+    );
     $("selectedUserLabel").textContent = `userId ${user.userId}`;
     $("userActionResult").textContent = "";
     $("userDetail").className = "detail-body";
     $("userDetail").innerHTML = `
@@
-        <section class="action-box">
-            <p class="section-title">멤버십 활성화</p>
-            ${reasonControl("membershipReason", "멤버십 활성화 사유", ["결제 확인", "결제 보정", "이벤트 지급", "기타"])}
-            <button id="membershipActivateButton" class="secondary">멤버십 활성화</button>
-        </section>
+        ${canActivateMembership ? `
+        <section class="action-box">
+            <p class="section-title">멤버십 활성화</p>
+            ${reasonControl("membershipReason", "멤버십 활성화 사유", ["결제 확인", "결제 보정", "이벤트 지급", "기타"])}
+            <button id="membershipActivateButton" class="secondary">멤버십 활성화</button>
+        </section>
+        ` : ""}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@manabom/src/main/resources/static/admin/app.js` around lines 343 - 347, The
membership activation UI is shown to all admins but backend
AdminWalletService.activateMembership allows only SUPER_ADMIN/FINANCE; update
the front-end to check state.admin.roles and either hide the whole <section> or
disable the membershipActivateButton and reasonControl unless the current admin
has role SUPER_ADMIN or FINANCE. Locate the section that renders
membershipActivateButton and reasonControl, add a role check against
state.admin.roles (allowing "SUPER_ADMIN" or "FINANCE") and conditionally render
or set the button to disabled with a tooltip/aria-disabled to prevent
unauthorized clicks.

438-450: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

쓰기 성공과 후속 새로고침 실패를 같은 실패로 처리하지 마세요.

Line 439의 POST가 성공한 뒤 loadUserDetail 또는 loadUsers만 실패해도 catch로 떨어져 "멤버십 활성화 실패"가 표시됩니다. 실제 서버 반영과 감사 로그는 이미 끝난 상태라, 운영자가 재시도하면서 중복 활성화/중복 감사 로그를 만들 수 있습니다. 쓰기 성공 여부와 화면 재동기화 실패를 분리해서 처리하는 게 맞습니다.

수정 예시
     try {
         await request(`/api/admin/users/${state.selectedUserId}/wallet/membership`, {
             method: "POST",
             body: {
                 reason
             }
         });
         $("membershipReasonSelect").value = "결제 확인";
         $("membershipReasonOther").value = "";
         $("membershipReasonOther").classList.add("hidden");
-        await loadUserDetail(state.selectedUserId);
-        await loadUsers();
         showUserActionResult("멤버십이 활성화되었습니다.");
+        try {
+            await loadUserDetail(state.selectedUserId);
+            await loadUsers();
+        } catch (refreshError) {
+            showUserActionResult("멤버십은 활성화됐지만 화면 동기화에 실패했습니다. 다시 조회해 주세요.", true);
+        }
     } catch (error) {
         showUserActionResult(`멤버십 활성화 실패: ${error.message}`, true);
     } finally {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@manabom/src/main/resources/static/admin/app.js` around lines 438 - 450, The
POST request for activating membership
(request(`/api/admin/users/${state.selectedUserId}/wallet/membership`)) should
be treated separately from UI refresh errors: after the await request call
succeeds, immediately call showUserActionResult("멤버십이 활성화되었습니다.") (or similar
success messaging) to reflect the committed write, then perform
loadUserDetail(state.selectedUserId) and loadUsers() inside their own try/catch;
if those refresh calls fail, catch and log/display a non-fatal warning (e.g.,
"멤버십은 활성화되었으나 화면 동기화에 실패했습니다.") rather than treating it as a full activation
failure, and avoid retrying the POST to prevent duplicate activations or audit
entries. Ensure you reference the same symbols: request, state.selectedUserId,
loadUserDetail, loadUsers, and showUserActionResult when applying this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@manabom/src/main/java/mannabom_server/manabom/presentation/admin/controller/AdminWalletController.java`:
- Around line 65-69: The current logic in AdminWalletController that returns
forwarded.split(",")[0].trim() can propagate empty or garbage hops; change the
handling of the "X-Forwarded-For" header in the method where forwarded and
remoteAddr are used so you iterate over forwarded.split(",") tokens, trim each
token and select the first non-blank and non-"unknown" (case-insensitive) token
as the client IP, and if none found return remoteAddr; update the code paths
that call AdminWalletService.activateMembership(...) and any audit logging to
use this validated IP.

---

Outside diff comments:
In `@manabom/src/main/resources/static/admin/app.js`:
- Around line 343-347: The membership activation UI is shown to all admins but
backend AdminWalletService.activateMembership allows only SUPER_ADMIN/FINANCE;
update the front-end to check state.admin.roles and either hide the whole
<section> or disable the membershipActivateButton and reasonControl unless the
current admin has role SUPER_ADMIN or FINANCE. Locate the section that renders
membershipActivateButton and reasonControl, add a role check against
state.admin.roles (allowing "SUPER_ADMIN" or "FINANCE") and conditionally render
or set the button to disabled with a tooltip/aria-disabled to prevent
unauthorized clicks.
- Around line 438-450: The POST request for activating membership
(request(`/api/admin/users/${state.selectedUserId}/wallet/membership`)) should
be treated separately from UI refresh errors: after the await request call
succeeds, immediately call showUserActionResult("멤버십이 활성화되었습니다.") (or similar
success messaging) to reflect the committed write, then perform
loadUserDetail(state.selectedUserId) and loadUsers() inside their own try/catch;
if those refresh calls fail, catch and log/display a non-fatal warning (e.g.,
"멤버십은 활성화되었으나 화면 동기화에 실패했습니다.") rather than treating it as a full activation
failure, and avoid retrying the POST to prevent duplicate activations or audit
entries. Ensure you reference the same symbols: request, state.selectedUserId,
loadUserDetail, loadUsers, and showUserActionResult when applying this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 95601d14-19ba-4ddf-8137-50bb434fb2e1

📥 Commits

Reviewing files that changed from the base of the PR and between 71dbd2a and 85c91cb.

📒 Files selected for processing (4)
  • manabom/src/main/java/mannabom_server/manabom/presentation/admin/controller/AdminWalletController.java
  • manabom/src/main/resources/application-prod.yml
  • manabom/src/main/resources/application.yml
  • manabom/src/main/resources/static/admin/app.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant