Skip to content

fix: [DM-50] 인증/인가 시스템 통합 검증 및 미구현 비즈니스 로직 전수 조사#18

Open
ParkGoeun00 wants to merge 5 commits intodevfrom
fix/DM-50-member-test-and-review
Open

fix: [DM-50] 인증/인가 시스템 통합 검증 및 미구현 비즈니스 로직 전수 조사#18
ParkGoeun00 wants to merge 5 commits intodevfrom
fix/DM-50-member-test-and-review

Conversation

@ParkGoeun00
Copy link
Collaborator

@ParkGoeun00 ParkGoeun00 commented Mar 11, 2026

관련 이슈

작업 내용

  • 테스트 코드 작성 및 실행
  • 초대, 프로젝트 나가기, 권한 변경 등 오류 수정 및 예외처리 추가
  • 코드 리팩터링

체크 리스트

  • PR 제목 규칙을 준수했습니다
  • 관련 이슈를 연결했습니다
  • 본문 내용을 명확하게 작성했습니다
  • 정상 작동을 로컬 환경에서 검증했습니다

@qodo-code-review
Copy link

Review Summary by Qodo

Authentication/Authorization System Integration Verification and Business Logic Implementation

🧪 Tests 🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
• Comprehensive test suite covering authentication, authorization, and member management services
  with 1,754 lines of unit tests
• Tests for InvitationService, ProjectService, AuthService, RedisTokenService,
  TokenProvider, MemberService, and CompanyService
• Extracted authentication logic from AuthApiController into new AuthService class for better
  separation of concerns
• Added member role change and removal operations to ProjectService with manager count validation
  and CEO protection
• Enhanced OAuth2 login flow with invite flow validation to prevent CEO role selection during
  invitations
• Improved JWT token validation by refactoring TokenProvider.validateToken() to throw exceptions
  instead of returning boolean
• Added invite flow support with URL encoding, error handling, and company assignment logic in
  InvitationService
• Fixed template fragment paths and corrected ProjectSettingPageData DTO field type from
  ProjectDetail to ProjectSummary
• Enhanced project settings UI with improved role change function including self-demotion validation
  and confirmation dialogs
• Added new API endpoints for changing project member roles and removing members with proper
  authorization
• Improved JWT filter exception handling with explicit catch blocks for expired and invalid tokens
• Updated security configuration to support invite acceptance flow with CSRF exemption and
  authentication requirements
Diagram
flowchart LR
  A["AuthApiController"] -->|delegates| B["AuthService"]
  B -->|uses| C["TokenProvider"]
  B -->|uses| D["RedisTokenService"]
  E["ProjectService"] -->|new methods| F["changeProjectRole<br/>removeProjectMember"]
  G["OAuth2SuccessHandler"] -->|validates| H["InviteFlow"]
  H -->|prevents| I["CEO Role Selection"]
  J["InvitationService"] -->|assigns| K["Company"]
  L["ProjectRepository"] -->|eager loads| M["Company"]
  N["JwtAuthenticationFilter"] -->|throws| O["ExpiredJwtException<br/>UnauthorizedException"]
  P["SecurityConfig"] -->|allows| Q["/invite/accept"]
Loading

Grey Divider

File Changes

1. src/test/java/kr/java/documind/domain/member/service/InvitationServiceTest.java 🧪 Tests +607/-0

Complete unit tests for invitation service functionality

• Comprehensive test suite for InvitationService with 607 lines covering invitation lifecycle
• Tests for sendInvitation() including normal flow, self-invitation rejection, duplicate pending
 invitations, and project not found scenarios
• Tests for resolveToken() covering Redis failures, token expiration, and database lookup edge
 cases
• Tests for acceptInvitation() validating email matching, company switching logic, CEO protection
 rules, and ProjectMember creation/reactivation

src/test/java/kr/java/documind/domain/member/service/InvitationServiceTest.java


2. src/test/java/kr/java/documind/domain/member/service/ProjectServiceTest.java 🧪 Tests +334/-0

Unit tests for project service operations

• Test suite for ProjectService with 334 lines covering project lifecycle operations
• Tests for createProject() validating company approval status and authorization
• Tests for leaveProject() and deleteProject() with CEO protection rules
• Tests for issueApiKey() covering new key generation and existing key revocation scenarios

src/test/java/kr/java/documind/domain/member/service/ProjectServiceTest.java


3. src/test/java/kr/java/documind/domain/auth/service/AuthServiceTest.java 🧪 Tests +304/-0

Unit tests for authentication service token operations

• Test suite for AuthService with 304 lines covering authentication token operations
• Tests for refresh() method validating token validation, Redis consistency, and account status
 checks
• Tests for logout() method covering authenticated/unauthenticated states, token blacklisting, and
 edge cases
• Comprehensive exception handling validation for expired tokens, invalid tokens, and suspended
 accounts

src/test/java/kr/java/documind/domain/auth/service/AuthServiceTest.java


View more (30)
4. src/test/java/kr/java/documind/global/security/RedisTokenServiceTest.java 🧪 Tests +343/-0

Unit tests for Redis token service operations

• Test suite for RedisTokenService with 343 lines covering Redis token management
• Tests for refresh token operations: save, get, consume, and delete
• Tests for token blacklisting with boundary value testing (0ms, 1ms, negative TTL)
• Tests for member suspension state management and token revocation

src/test/java/kr/java/documind/global/security/RedisTokenServiceTest.java


5. src/test/java/kr/java/documind/global/security/jwt/TokenProviderTest.java 🧪 Tests +285/-0

Unit tests for JWT token provider functionality

• Test suite for TokenProvider with 285 lines covering JWT token generation and validation
• Tests for access and refresh token generation with payload extraction validation
• Tests for token validation covering expired tokens, tampered signatures, and empty strings
• Tests for token ID extraction and remaining milliseconds calculation

src/test/java/kr/java/documind/global/security/jwt/TokenProviderTest.java


6. src/test/java/kr/java/documind/domain/member/service/MemberServiceTest.java 🧪 Tests +278/-0

Unit tests for member service operations

• Test suite for MemberService with 278 lines covering member operations
• Tests for getMember() and getMemberWithCompany() with existence validation
• Tests for findOrCreateOAuthMember() covering existing member lookup and new member creation
• Tests for profile updates and profile image uploads with file store integration

src/test/java/kr/java/documind/domain/member/service/MemberServiceTest.java


7. src/test/java/kr/java/documind/domain/member/service/CompanyServiceTest.java 🧪 Tests +207/-0

Unit tests for company service operations

• Test suite for CompanyService with 207 lines covering company management operations
• Tests for approveCompany() and rejectCompany() with status transitions
• Tests for registerCompany() validating company creation and conflict detection
• Tests for updateCompanyName() with authorization and existence checks

src/test/java/kr/java/documind/domain/member/service/CompanyServiceTest.java


8. src/main/java/kr/java/documind/domain/auth/controller/AuthApiController.java Refactoring +16/-89

Refactor authentication controller to use AuthService

• Refactored token refresh and logout logic into AuthService for better separation of concerns
• Simplified refresh() endpoint by delegating to authService.refresh() which handles all
 validation
• Simplified logout() endpoint by delegating to authService.logout() with unified token cleanup
 logic
• Removed direct dependencies on TokenProvider, RedisTokenService, and MemberService

src/main/java/kr/java/documind/domain/auth/controller/AuthApiController.java


9. src/main/java/kr/java/documind/domain/member/service/ProjectService.java ✨ Enhancement +68/-4

Add member role change and removal operations

• Added changeProjectRole() method to change member project roles with manager count validation
• Added removeProjectMember() method to remove members from projects with CEO protection
• Fixed variable naming from projectDetail to projectSummary for consistency
• Removed unused import of ProjectDetail DTO

src/main/java/kr/java/documind/domain/member/service/ProjectService.java


10. src/main/java/kr/java/documind/global/security/oauth/OAuth2SuccessHandler.java Refactoring +27/-35

Refactor OAuth2 success handler redirect logic

• Refactored redirect URL determination into separate determineRedirectUrl() method
• Extracted default redirect logic into resolveDefaultRedirectUrl() method
• Added invite flow validation with role mismatch toast message handling
• Improved code organization by moving cookie cleanup before token generation

src/main/java/kr/java/documind/global/security/oauth/OAuth2SuccessHandler.java


11. src/main/java/kr/java/documind/global/security/oauth/CustomOAuth2UserService.java ✨ Enhancement +31/-4

Add invite flow validation for OAuth2 login

• Added INVITE_FLOW_VALIDATION_ERROR constant for invite flow validation
• Added isInviteFlow() method to detect if user is in invitation acceptance flow
• Added validation to prevent CEO role selection during invite flow
• Moved static constants to top of class for better organization

src/main/java/kr/java/documind/global/security/oauth/CustomOAuth2UserService.java


12. src/main/java/kr/java/documind/domain/auth/service/AuthService.java ✨ Enhancement +98/-0

New authentication service for token operations

• New service class extracting authentication logic from AuthApiController
• Implements refresh() method with comprehensive token validation and member status checks
• Implements logout() method handling both authenticated and unauthenticated logout scenarios
• Defines AuthTokens record for returning access and refresh token pair

src/main/java/kr/java/documind/domain/auth/service/AuthService.java


13. src/main/java/kr/java/documind/domain/member/controller/MyPageViewController.java 🐞 Bug fix +1/-23

Update admin redirect path and remove unused imports

• Removed unused CompanyService and AdminPageData imports
• Updated admin redirect from /my/company/admin to /admin/companies
• Simplified controller by removing unused dependency injection

src/main/java/kr/java/documind/domain/member/controller/MyPageViewController.java


14. src/main/java/kr/java/documind/domain/member/controller/ProjectApiController.java ✨ Enhancement +26/-0

Add member role change and removal API endpoints

• Added changeProjectMemberRole() endpoint for updating member project roles
• Added removeProjectMember() endpoint for removing members from projects
• Both endpoints require @RequireProjectManager authorization
• Added ProjectRoleUpdateRequest DTO import and UUID import

src/main/java/kr/java/documind/domain/member/controller/ProjectApiController.java


15. src/main/java/kr/java/documind/global/security/filter/JwtAuthenticationFilter.java Error handling +33/-21

Improve JWT filter exception handling

• Improved exception handling with explicit catch blocks for ExpiredJwtException and
 UnauthorizedException
• Added try-catch wrapper around token validation to handle all JWT-related exceptions
• Enhanced logging for expired tokens and invalid tokens with specific error messages
• Refactored validation logic to be more explicit and maintainable

src/main/java/kr/java/documind/global/security/filter/JwtAuthenticationFilter.java


16. src/main/java/kr/java/documind/domain/member/controller/InviteViewController.java ✨ Enhancement +17/-5

Enhance invite flow error handling and logging

• Added URL encoding for redirect URL in login flow
• Enhanced error handling with separate view for email mismatch errors
• Added mismatch flag to model for email mismatch scenarios
• Improved logging with redirect URL details and token acceptance tracking

src/main/java/kr/java/documind/domain/member/controller/InviteViewController.java


17. src/main/java/kr/java/documind/domain/member/service/InvitationService.java ✨ Enhancement +18/-4

Enhance invitation acceptance with company assignment

• Added logging for invitation acceptance start with token hash prefix
• Added company assignment logic for members without company affiliation
• Refactored project lookup to use findByPublicIdWithCompany() for eager loading
• Improved error handling and logging for company switching scenarios

src/main/java/kr/java/documind/domain/member/service/InvitationService.java


18. src/main/java/kr/java/documind/domain/member/model/dto/ProjectSettingPageData.java 🐞 Bug fix +1/-1

Fix ProjectSettingPageData DTO field type

• Changed field type from ProjectDetail to ProjectSummary for consistency
• Updated record definition to use correct DTO type

src/main/java/kr/java/documind/domain/member/model/dto/ProjectSettingPageData.java


19. src/main/java/kr/java/documind/global/config/SecurityConfig.java ✨ Enhancement +9/-2

Security configuration updates for invite acceptance flow

• Added /invite/accept endpoint to CSRF ignore list for unauthenticated access
• Added authentication requirement for POST requests to /invite/accept endpoint
• Added explicit permit-all rule for OAuth2-related paths (/oauth2/**, /login/oauth2/**)

src/main/java/kr/java/documind/global/config/SecurityConfig.java


20. src/main/java/kr/java/documind/global/security/jwt/TokenProvider.java ✨ Enhancement +4/-6

Token validation refactored to throw exceptions

• Changed validateToken() method from returning boolean to void
• Modified to throw ExpiredJwtException directly instead of returning false
• Added UnauthorizedException for invalid token cases instead of returning false
• Removed debug/warn logging statements

src/main/java/kr/java/documind/global/security/jwt/TokenProvider.java


21. src/main/java/kr/java/documind/domain/member/controller/AdminViewController.java ✨ Enhancement +39/-0

New admin company management view controller

• New controller class for admin company management view
• Handles GET request to /admin/companies endpoint
• Validates user has ADMIN role, redirects to /my/company if not
• Prepares admin page data with company statistics and categorized company lists

src/main/java/kr/java/documind/domain/member/controller/AdminViewController.java


22. src/main/java/kr/java/documind/domain/member/model/repository/ProjectMemberRepository.java ✨ Enhancement +4/-0

Repository method for counting project members by role

• Added new query method countByProjectAndProjectRole() to count members by project and role
• Used for preventing demotion of the last project manager

src/main/java/kr/java/documind/domain/member/model/repository/ProjectMemberRepository.java


23. src/main/java/kr/java/documind/domain/auth/controller/AuthViewController.java ✨ Enhancement +11/-1

Login page enhanced with invite flow and redirect support

• Added flow and redirect request parameters to login page handler
• Added model attributes isInviteFlow and redirectUrl for template rendering
• Supports invite flow detection and post-login redirect functionality

src/main/java/kr/java/documind/domain/auth/controller/AuthViewController.java


24. src/main/java/kr/java/documind/domain/auth/model/repository/ProjectRepository.java ✨ Enhancement +3/-0

Project repository method with eager company loading

• Added new query method findByPublicIdWithCompany() with JOIN FETCH for company
• Optimizes database queries by eagerly loading associated company data

src/main/java/kr/java/documind/domain/auth/model/repository/ProjectRepository.java


25. src/main/java/kr/java/documind/domain/member/model/dto/ProjectRoleUpdateRequest.java ✨ Enhancement +6/-0

New DTO for project role update requests

• New DTO record for project role update requests
• Contains single field role of type ProjectRole with NotNull validation

src/main/java/kr/java/documind/domain/member/model/dto/ProjectRoleUpdateRequest.java


26. src/main/resources/templates/member/project-setting.html Formatting +104/-98

Project settings template formatting and role change improvements

• Fixed indentation and formatting of project dropdown menu HTML structure
• Updated role change select element to pass entire element reference and include data-is-me
 attribute
• Modified changeRole() function call to pass this instead of individual parameters
• Reordered script loading to ensure _PS object is defined before ProjectSetting.js executes
• Fixed table header and body indentation for better readability

src/main/resources/templates/member/project-setting.html


27. src/main/resources/templates/auth/login.html ✨ Enhancement +11/-5

Login template enhanced for invite flow and redirect handling

• Added conditional invite flow guidance message when isInviteFlow is true
• Hidden CEO/admin role selection button during invite flow
• Updated OAuth2 authorization links to include redirect_after_login parameter
• Changed GitHub button text from "GitHub 계정으로 로그인하기" to "GitHub로 계속하기"

src/main/resources/templates/auth/login.html


28. src/main/resources/templates/common/fragments/header-auth.html Formatting +2/-3

Header fragment template syntax and formatting fixes

• Removed leading blank line
• Updated Thymeleaf fragment replacement syntax from ::user-info-area to ~{::user-info-area} for
 consistency

src/main/resources/templates/common/fragments/header-auth.html


29. src/main/resources/templates/member/invite-company-leave.html 🐞 Bug fix +1/-1

Corrected template fragment path reference

• Fixed head fragment path from common/head to common/fragments/head

src/main/resources/templates/member/invite-company-leave.html


30. src/main/resources/templates/member/invite-confirm.html 🐞 Bug fix +1/-1

Corrected template fragment path reference

• Fixed head fragment path from common/head to common/fragments/head

src/main/resources/templates/member/invite-confirm.html


31. src/main/resources/templates/member/invite-error.html 🐞 Bug fix +1/-1

Corrected template fragment path reference

• Fixed head fragment path from common/head to common/fragments/head

src/main/resources/templates/member/invite-error.html


32. src/main/resources/static/js/member/ProjectSetting.js ✨ Enhancement +23/-4

Project role change function enhanced with validation and UX improvements

• Refactored changeRole() function to accept select element instead of individual parameters
• Added validation to prevent current user from demoting themselves without confirmation
• Improved error handling with proper select value restoration on API failure
• Added success alert and page reload when user's own role changes
• Enhanced user experience with confirmation dialog for self-demotion

src/main/resources/static/js/member/ProjectSetting.js


33. src/main/java/kr/java/documind/domain/member/model/dto/ProjectDetail.java Additional files +0/-3

...

src/main/java/kr/java/documind/domain/member/model/dto/ProjectDetail.java


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Mar 11, 2026

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Refresh leaves invalid cookies🐞 Bug ⛯ Reliability
Description
AuthApiController.refresh()는 AuthService.refresh()가 예외(Unauthorized/Forbidden)를 던지는 경우를 처리하지 않아, 실패
시에도 access/refresh 쿠키가 그대로 남습니다. 이 상태로는 브라우저가 계속 잘못된 refresh 쿠키를 보내 401이 반복되고 사용자가 쿠키를 수동 삭제하기 전까지
복구가 어려워집니다.
Code

src/main/java/kr/java/documind/domain/auth/controller/AuthApiController.java[R36-55]

                       .getCookieValue(request, jwtProperties.getRefreshCookieName())
                       .orElse(null);

-        if (refreshToken == null) {
-            return ResponseEntity.status(401)
-                    .body(ApiResponse.error(ErrorResponse.of("Refresh Token이 없습니다.")));
-        }
-
-        if (!jwtProvider.validateToken(refreshToken)) {
-            log.debug("만료 또는 유효하지 않은 Refresh Token");
-            deleteAuthCookies(response);
-            return ResponseEntity.status(401)
-                    .body(
-                            ApiResponse.error(
-                                    ErrorResponse.of("Refresh Token이 만료되었습니다. 다시 로그인하세요.")));
-        }
-
-        UUID memberId = jwtProvider.getMemberId(refreshToken);
-        GlobalRole globalRole = jwtProvider.getGlobalRole(refreshToken);
-
-        String storedToken = redisTokenService.consumeRefreshToken(memberId);
-        if (!refreshToken.equals(storedToken)) {
-            log.warn("Refresh Token 불일치 — 탈취 가능성: memberId={}", memberId);
-            deleteAuthCookies(response);
-            return ResponseEntity.status(401)
-                    .body(
-                            ApiResponse.error(
-                                    ErrorResponse.of("유효하지 않은 Refresh Token입니다. 다시 로그인하세요.")));
-        }
-
-        Member member = memberService.getMemberWithCompany(memberId);
-        if (!member.isActive()) {
-            log.warn(
-                    "[AuthApiController] 비활성 계정의 토큰 갱신 시도: memberId={} status={}",
-                    memberId,
-                    member.getAccountStatus());
-            deleteAuthCookies(response);
-            return ResponseEntity.status(401)
-                    .body(ApiResponse.error(ErrorResponse.of("계정이 비활성화되었습니다. 다시 로그인하세요.")));
-        }
-
-        String newAccessToken = jwtProvider.generateAccessToken(memberId, globalRole);
-        String newRefreshToken = jwtProvider.generateRefreshToken(memberId, globalRole);
+        AuthTokens newTokens = authService.refresh(refreshToken);

       boolean secure = jwtProperties.isCookieSecure();
       cookieUtil.addCookie(
               response,
               jwtProperties.getAccessCookieName(),
-                newAccessToken,
+                newTokens.accessToken(),
               jwtProperties.getAccessExpirationSeconds(),
               secure);
       cookieUtil.addCookie(
               response,
               jwtProperties.getRefreshCookieName(),
-                newRefreshToken,
+                newTokens.refreshToken(),
               jwtProperties.getRefreshExpirationSeconds(),
               secure);

-        redisTokenService.saveRefreshToken(
-                memberId, newRefreshToken, jwtProperties.getRefreshExpirationSeconds());
-
-        log.debug("[AuthApiController] Access Token 재발급 완료: memberId={}", memberId);
       return ResponseEntity.ok(ApiResponse.success(null));
Evidence
컨트롤러는 refresh 재발급 호출만 하고 예외 발생 시 쿠키 삭제를 수행하지 않습니다. 반면 AuthService.refresh()는 refreshToken 누락/만료/불일치
등에 대해 예외를 던지므로, 이 예외 경로에서 쿠키 정리가 누락된 것이 코드로 확인됩니다.

src/main/java/kr/java/documind/domain/auth/controller/AuthApiController.java[30-56]
src/main/java/kr/java/documind/domain/auth/service/AuthService.java[28-49]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`POST /api/auth/refresh`에서 refresh token이 만료/불일치/누락인 경우 `AuthService.refresh()`가 예외를 던지지만, 컨트롤러가 이를 처리하지 않아 auth 쿠키가 남아 반복 401 및 복구 불가 상태가 됩니다.
## Issue Context
- `AuthApiController.refresh()`는 성공 시에만 새 쿠키를 설정하며, 실패 시 쿠키 삭제 로직이 없습니다.
- `AuthService.refresh()`는 정상적으로 예외를 던지도록 구현되어 있습니다.
## Fix Focus Areas
- src/main/java/kr/java/documind/domain/auth/controller/AuthApiController.java[30-56]
- src/main/java/kr/java/documind/domain/auth/service/AuthService.java[28-49]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Invite token logged🐞 Bug ⛨ Security
Description
InviteViewController.acceptInvite()가 초대 토큰 원문을 INFO 로그로 출력합니다. 초대 수락은 해당 토큰을 자격증명으로 사용하므로 로그 유출 시 토큰
재사용(리플레이)로 프로젝트 합류가 가능해집니다.
Code

src/main/java/kr/java/documind/domain/member/controller/InviteViewController.java[R91-95]

+        log.info("[InviteViewController] acceptInvite 요청 받음: token={}", token);
+
       try {
           String publicId =
                   invitationService.acceptInvitation(
Evidence
컨트롤러가 token 값을 그대로 로그에 남기고, 서비스는 rawToken으로 초대 검증/수락을 수행합니다. 즉 로그에 남는 token은 민감한 bearer 성격을 갖습니다.

src/main/java/kr/java/documind/domain/member/controller/InviteViewController.java[84-104]
src/main/java/kr/java/documind/domain/member/service/InvitationService.java[129-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
초대 수락 요청에서 초대 토큰(raw token)을 INFO 로그로 남기고 있어, 로그 접근만으로 초대 수락 재시도(리플레이)가 가능해집니다.
## Issue Context
토큰은 초대 수락의 핵심 자격증명이므로 원문을 로그에 남기면 안 됩니다.
## Fix Focus Areas
- src/main/java/kr/java/documind/domain/member/controller/InviteViewController.java[84-104]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Manager count includes deleted🐞 Bug ✓ Correctness
Description
ProjectService.changeProjectRole()의 마지막 관리자 강등 방지 로직이 countByProjectAndProjectRole()을 사용하는데, 이 카운트는
DELETED 멤버십을 제외하지 않습니다. 그 결과 삭제된 관리자 행이 남아있으면 실제로는 마지막 활성 관리자인데도 강등이 허용될 수 있습니다.
Code

src/main/java/kr/java/documind/domain/member/service/ProjectService.java[R264-268]

+        if (targetPm.isManager() && newRole == ProjectRole.MEMBER) {
+            if (projectMemberRepository.countByProjectAndProjectRole(project, ProjectRole.MANAGER)
+                    <= 1) {
+                throw new BadRequestException("프로젝트에는 최소 한 명 이상의 관리자가 필요합니다.");
+            }
Evidence
강등 방지 조건은 MANAGER 수를 세지만, repository 카운트 메서드는 status 조건이 없고 ProjectMember.softDelete()는 status를
DELETED로 바꿉니다. 따라서 DELETED 관리자도 카운트에 포함되어 ‘활성 관리자 수’ 검증이 틀어집니다.

src/main/java/kr/java/documind/domain/member/service/ProjectService.java[264-268]
src/main/java/kr/java/documind/domain/member/model/repository/ProjectMemberRepository.java[58-62]
src/main/java/kr/java/documind/domain/member/model/entity/ProjectMember.java[73-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
마지막 관리자 강등 방지 로직이 소프트삭제(DELETED) 멤버십까지 포함해 카운트하여, 활성 관리자 0명 상태를 만들 수 있습니다.
## Issue Context
`ProjectMember.softDelete()`는 `status=DELETED`로 변경됩니다.
## Fix Focus Areas
- src/main/java/kr/java/documind/domain/member/model/repository/ProjectMemberRepository.java[58-62]
- src/main/java/kr/java/documind/domain/member/service/ProjectService.java[264-268]
- src/main/java/kr/java/documind/domain/member/model/entity/ProjectMember.java[73-76]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Last manager removable🐞 Bug ✓ Correctness
Description
ProjectService.removeProjectMember()는 대상이 MANAGER인지/마지막 MANAGER인지 검사 없이 멤버십을 softDelete()합니다. 이 경로로
마지막 관리자를 제거하면 프로젝트가 관리자 0명 상태가 됩니다.
Code

src/main/java/kr/java/documind/domain/member/service/ProjectService.java[R280-303]

+    @Transactional
+    public void removeProjectMember(String publicId, UUID actorMemberId, UUID targetMemberId) {
+        if (actorMemberId.equals(targetMemberId)) {
+            throw new BadRequestException("자기 자신을 제거할 수 없습니다. '프로젝트 나가기'를 이용해주세요.");
+        }
+
+        Project project =
+                projectRepository
+                        .findByPublicId(publicId)
+                        .orElseThrow(ProjectNotFoundException::new);
+
+        Member targetMember = memberService.getMember(targetMemberId);
+
+        if (targetMember.isCeo()) {
+            throw new ForbiddenException("대표(CEO)는 프로젝트에서 제거할 수 없습니다.");
+        }
+
+        ProjectMember targetPm =
+                projectMemberRepository
+                        .findByProjectAndMember(project, targetMember)
+                        .orElseThrow(() -> new NotFoundException("대상이 프로젝트 멤버가 아닙니다."));
+
+        targetPm.softDelete();
+        log.info(
Evidence
changeProjectRole()에는 최소 1명 관리자 유지 규칙이 있지만, removeProjectMember()에는 동일한 보호 로직이 없어 불변식이 깨집니다.

src/main/java/kr/java/documind/domain/member/service/ProjectService.java[280-303]
src/main/java/kr/java/documind/domain/member/service/ProjectService.java[264-268]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`removeProjectMember()`가 마지막 관리자 제거를 막지 않아 프로젝트에 관리자 0명 상태가 될 수 있습니다.
## Issue Context
권한 강등(changeProjectRole)에는 마지막 관리자 보호가 있으나, 멤버 제거에는 동일한 보호가 없습니다.
## Fix Focus Areas
- src/main/java/kr/java/documind/domain/member/service/ProjectService.java[280-303]
- src/main/java/kr/java/documind/domain/member/service/ProjectService.java[264-268]
- src/main/java/kr/java/documind/domain/member/model/repository/ProjectMemberRepository.java[58-62]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. CSRF disabled invite accept 🐞 Bug ⛨ Security
Description
SecurityConfig가 POST /invite/accept를 CSRF 검증에서 제외하지만, 실제 초대 수락 폼은 이미 CSRF 토큰을 전송하고 있습니다. 불필요한 예외 처리로
인해 인증된 상태 변경 엔드포인트의 방어선이 약화됩니다.
Code

src/main/java/kr/java/documind/global/config/SecurityConfig.java[R72-84]

                                       .csrfTokenRequestHandler(
                                               new CsrfTokenRequestAttributeHandler())
                                       .ignoringRequestMatchers(
-                                                "/oauth2/**", "/login/oauth2/**", "/api/**"))
+                                                "/oauth2/**",
+                                                "/login/oauth2/**",
+                                                "/api/**",
+                                                "/invite/accept"))
               .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()))
               .authorizeHttpRequests(
                       auth ->
-                                auth
+                                auth.requestMatchers(HttpMethod.POST, "/invite/accept")
+                                        .authenticated()
                                       // ── 공개 페이지 / 정적 리소스 ──────────────────────
Evidence
CSRF ignoring 목록에 /invite/accept가 추가되어 CSRF 체크가 우회됩니다. 그런데 invite-confirm / invite-company-leave 폼은
_csrf hidden input을 포함하고 있어, CSRF를 끌 이유가 코드상 확인되지 않습니다.

src/main/java/kr/java/documind/global/config/SecurityConfig.java[69-84]
src/main/resources/templates/member/invite-confirm.html[51-55]
src/main/resources/templates/member/invite-company-leave.html[81-86]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`/invite/accept`에 대해 CSRF 보호가 비활성화되어, state-changing 요청에 대한 방어가 불필요하게 약화됩니다(폼은 이미 CSRF 토큰을 포함).
## Issue Context
Thymeleaf 폼은 `_csrf` hidden input을 포함하고 있어 CSRF 검증을 활성화해도 정상 동작해야 합니다.
## Fix Focus Areas
- src/main/java/kr/java/documind/global/config/SecurityConfig.java[69-84]
- src/main/resources/templates/member/invite-confirm.html[51-55]
- src/main/resources/templates/member/invite-company-leave.html[81-86]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@greptile-apps
Copy link

greptile-apps bot commented Mar 11, 2026

Comments Outside Diff (4)

  1. src/main/java/kr/java/documind/domain/member/service/ProjectService.java, line 732-749 (link)

    Missing last-manager guard in leaveProject

    changeProjectRole prevents the last manager from being demoted by checking countByProjectAndProjectRole(project, ProjectRole.MANAGER) <= 1, but leaveProject has no equivalent check. A project MANAGER can leave the project even if they are the only non-CEO manager, potentially leaving the project without an active manager (only the CEO remains). This is inconsistent with the intent of changeProjectRole.

  2. src/main/java/kr/java/documind/domain/member/service/ProjectService.java, line 702-730 (link)

    Missing last-manager guard in removeProjectMember

    Like leaveProject, this new method does not verify whether the target member is the last remaining MANAGER before removing them. A manager actor can remove another manager, leaving the project with only the CEO as the effective manager — inconsistent with the protection applied in changeProjectRole.

    Consider adding the same guard that exists in changeProjectRole:

    ProjectMember targetPm = ...
    
    if (targetPm.isManager()
            && projectMemberRepository.countByProjectAndProjectRole(project, ProjectRole.MANAGER) <= 1) {
        throw new BadRequestException("프로젝트에는 최소 한 명 이상의 관리자가 필요합니다.");
    }
    
    targetPm.softDelete();
    
  3. src/main/java/kr/java/documind/domain/auth/service/AuthService.java, line 248-250 (link)

    UnauthorizedException catch block may silently swallow unrelated errors

    The catch block for UnauthorizedException re-throws with the message "유효하지 않은 Refresh Token입니다.". However, TokenProvider.validateToken throws UnauthorizedException wrapping a JwtException | IllegalArgumentException. If any other UnauthorizedException is thrown from within validateToken for a different reason in the future (or via a dependency), it will be silently replaced with this generic message, masking the original error.

    Consider catching the specific exception type thrown by validateToken more narrowly — or at minimum log the original cause before re-wrapping:

  4. src/main/resources/templates/auth/login.html, line 141-143 (link)

    Redundant redirect_after_login URL parameter may conflict with cookie

    The REDIRECT_AFTER_LOGIN_COOKIE is already set by InviteViewController before this login page is rendered. Passing the same value as the redirect_after_login URL parameter in the OAuth2 button href causes HttpCookieOAuth2AuthorizationRequestRepository.saveAuthorizationRequest to overwrite the cookie with a re-encoded copy of the same value. In the normal flow this is harmless, but it creates an implicit dependency between these two mechanisms.

    If redirectUrl is non-null (invite flow), the invite redirect URL is set twice: once via the cookie in InviteViewController, and once via this URL parameter. A comment explaining the dual-write intent would help future maintainers understand why both are needed.

Last reviewed commit: e5a194b

@ParkGoeun00
Copy link
Collaborator Author

@greptileai review

@greptile-apps

This comment was marked as resolved.

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