Skip to content

Commit e850fd1

Browse files
authored
Merge pull request #235 from CSE-Shaco/develop
[FEAT] 사용자 권한 관리 기능 추가 및 인증/리크루팅 권한 구조 개선
2 parents b9c593c + eb67c74 commit e850fd1

File tree

12 files changed

+344
-36
lines changed

12 files changed

+344
-36
lines changed

src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import inha.gdgoc.domain.auth.service.MailService;
1515
import inha.gdgoc.domain.auth.service.RefreshTokenService;
1616
import inha.gdgoc.domain.user.entity.User;
17+
import inha.gdgoc.domain.user.enums.TeamType;
1718
import inha.gdgoc.domain.user.enums.UserRole;
1819
import inha.gdgoc.domain.user.repository.UserRepository;
1920
import inha.gdgoc.global.config.jwt.TokenProvider;
@@ -165,15 +166,30 @@ public ResponseEntity<ApiResponse<Void, Void>> resetPassword(@RequestBody Passwo
165166
* 예) /api/v1/auth/LEAD, /api/v1/auth/ORGANIZER, /api/v1/auth/ADMIN
166167
*/
167168
@GetMapping("/{role}")
168-
public ResponseEntity<ApiResponse<Void, ?>> checkRole(@AuthenticationPrincipal TokenProvider.CustomUserDetails me, @PathVariable UserRole role) {
169+
public ResponseEntity<ApiResponse<Void, ?>> checkRoleOrTeam(@AuthenticationPrincipal TokenProvider.CustomUserDetails me, @PathVariable UserRole role, @RequestParam(value = "team", required = false) TeamType requiredTeam) {
170+
// 1) 인증 체크
169171
if (me == null) {
170172
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
171173
.body(ApiResponse.error(GlobalErrorCode.UNAUTHORIZED_USER.getStatus()
172174
.value(), GlobalErrorCode.UNAUTHORIZED_USER.getMessage(), null));
173175
}
174176

175-
if (UserRole.hasAtLeast(me.getRole(), role)) {
176-
return ResponseEntity.ok(ApiResponse.ok("ROLE_CHECK_PASSED", null));
177+
// 2) role check
178+
final boolean roleOk = UserRole.hasAtLeast(me.getRole(), role);
179+
180+
// 3) team check if team parameter exists
181+
boolean teamOk = false;
182+
if (requiredTeam != null) {
183+
if (UserRole.hasAtLeast(me.getRole(), UserRole.ORGANIZER)) {
184+
teamOk = true;
185+
} else {
186+
teamOk = (me.getTeam() != null && me.getTeam() == requiredTeam);
187+
}
188+
}
189+
190+
// 4) OR 조건으로 최종 판정
191+
if (roleOk || teamOk) {
192+
return ResponseEntity.ok(ApiResponse.ok("ROLE_OR_TEAM_CHECK_PASSED", null));
177193
}
178194

179195
return ResponseEntity.status(HttpStatus.FORBIDDEN)

src/main/java/inha/gdgoc/domain/core/recruit/controller/CoreRecruitController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public ResponseEntity<ApiResponse<CreateResponse, Void>> create(
5252
description = "전체 목록 또는 이름 검색 결과를 반환합니다.",
5353
security = { @SecurityRequirement(name = "BearerAuth") }
5454
)
55-
@PreAuthorize("hasRole('ADMIN')")
55+
@PreAuthorize("hasAnyRole('LEAD', 'ORGANIZER', 'ADMIN')")
5656
@GetMapping("/applicants")
5757
public ResponseEntity<ApiResponse<java.util.List<CoreRecruitApplicantSummaryResponse>, PageMeta>> getApplicants(
5858
@Parameter(description = "검색어(이름 부분 일치). 없으면 전체 조회", example = "홍길동")
@@ -89,7 +89,7 @@ public ResponseEntity<ApiResponse<java.util.List<CoreRecruitApplicantSummaryResp
8989
summary = "코어 리쿠르트 지원자 상세 조회",
9090
security = { @SecurityRequirement(name = "BearerAuth") }
9191
)
92-
@PreAuthorize("hasRole('ADMIN')")
92+
@PreAuthorize("hasAnyRole('LEAD', 'ORGANIZER', 'ADMIN')")
9393
@GetMapping("/applicants/{id}")
9494
public ResponseEntity<ApiResponse<CoreRecruitApplicantDetailResponse, Void>> getApplicant(
9595
@PathVariable Long id

src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public ResponseEntity<ApiResponse<CheckPhoneNumberResponse, Void>> duplicatedPho
8585
}
8686

8787
@Operation(summary = "특정 멤버 가입 신청서 조회", security = {@SecurityRequirement(name = "BearerAuth")})
88-
@PreAuthorize("hasRole('ADMIN')")
88+
@PreAuthorize("hasAnyRole('LEAD','ORGANIZER','ADMIN') or T(inha.gdgoc.domain.user.enums.TeamType).HR == principal.team")
8989
@GetMapping("/recruit/members/{memberId}")
9090
public ResponseEntity<ApiResponse<SpecifiedMemberResponse, Void>> getSpecifiedMember(
9191
@PathVariable Long memberId
@@ -100,7 +100,7 @@ public ResponseEntity<ApiResponse<SpecifiedMemberResponse, Void>> getSpecifiedMe
100100
description = "설정하려는 상태(NOT 현재 상태)를 body에 보내주세요. true=입금 완료, false=입금 미완료",
101101
security = { @SecurityRequirement(name = "BearerAuth") }
102102
)
103-
@PreAuthorize("hasRole('ADMIN')")
103+
@PreAuthorize("hasAnyRole('LEAD','ORGANIZER','ADMIN') or T(inha.gdgoc.domain.user.enums.TeamType).HR == principal.team")
104104
@PatchMapping("/recruit/members/{memberId}/payment")
105105
public ResponseEntity<ApiResponse<Void, Void>> updatePayment(
106106
@PathVariable Long memberId,
@@ -122,7 +122,7 @@ public ResponseEntity<ApiResponse<Void, Void>> updatePayment(
122122
description = "전체 목록 또는 이름 검색 결과를 반환합니다. 검색어(question)를 주면 이름 포함 검색, 없으면 전체 조회. sort랑 dir은 example 값 그대로 코딩하는 것 추천...",
123123
security = { @SecurityRequirement(name = "BearerAuth") }
124124
)
125-
@PreAuthorize("hasRole('ADMIN')")
125+
@PreAuthorize("hasAnyRole('LEAD','ORGANIZER','ADMIN') or T(inha.gdgoc.domain.user.enums.TeamType).HR == principal.team")
126126
@GetMapping("/recruit/members")
127127
public ResponseEntity<ApiResponse<List<RecruitMemberSummaryResponse>, PageMeta>> getMembers(
128128
@Parameter(description = "검색어(이름 부분 일치). 없으면 전체 조회", example = "소연")
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package inha.gdgoc.domain.user.controller;
2+
3+
import inha.gdgoc.domain.user.dto.request.UpdateRoleRequest;
4+
import inha.gdgoc.domain.user.dto.request.UpdateUserRoleTeamRequest;
5+
import inha.gdgoc.domain.user.dto.response.UserSummaryResponse;
6+
import inha.gdgoc.domain.user.service.UserAdminService;
7+
import inha.gdgoc.global.config.jwt.TokenProvider.CustomUserDetails;
8+
import inha.gdgoc.global.dto.response.ApiResponse;
9+
import inha.gdgoc.global.dto.response.PageMeta;
10+
import io.swagger.v3.oas.annotations.Operation;
11+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
12+
import jakarta.validation.Valid;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.data.domain.Page;
15+
import org.springframework.data.domain.PageRequest;
16+
import org.springframework.data.domain.Pageable;
17+
import org.springframework.data.domain.Sort;
18+
import org.springframework.http.ResponseEntity;
19+
import org.springframework.security.access.prepost.PreAuthorize;
20+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
21+
import org.springframework.web.bind.annotation.*;
22+
23+
@RequiredArgsConstructor
24+
@RestController
25+
@RequestMapping("/api/v1/admin/users")
26+
public class UserAdminController {
27+
28+
private final UserAdminService userAdminService;
29+
30+
@Operation(summary = "사용자 요약 목록 조회", security = {@SecurityRequirement(name = "BearerAuth")})
31+
@PreAuthorize("hasAnyRole('LEAD','ORGANIZER','ADMIN') or T(inha.gdgoc.domain.user.enums.TeamType).HR == principal.team")
32+
@GetMapping
33+
public ResponseEntity<ApiResponse<Page<UserSummaryResponse>, PageMeta>> list(@RequestParam(required = false) String q, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(defaultValue = "name") String sort, @RequestParam(defaultValue = "ASC") String dir) {
34+
Sort.Direction direction = "ASC".equalsIgnoreCase(dir) ? Sort.Direction.ASC : Sort.Direction.DESC;
35+
Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sort));
36+
37+
Page<UserSummaryResponse> result = userAdminService.listUsers(q, pageable);
38+
return ResponseEntity.ok(ApiResponse.ok("USER_SUMMARY_LIST_RETRIEVED", result, PageMeta.of(result)));
39+
}
40+
41+
@Operation(summary = "사용자 역할/팀 수정", security = {@SecurityRequirement(name = "BearerAuth")})
42+
@PreAuthorize("hasAnyRole('LEAD','ORGANIZER','ADMIN')")
43+
@PatchMapping("/{userId}/role-team")
44+
public ResponseEntity<ApiResponse<Void, Void>> updateRoleTeam(@AuthenticationPrincipal CustomUserDetails me, @PathVariable Long userId, @RequestBody UpdateUserRoleTeamRequest req) {
45+
userAdminService.updateRoleAndTeam(me, userId, req);
46+
return ResponseEntity.ok(ApiResponse.ok("USER_ROLE_TEAM_UPDATED"));
47+
}
48+
49+
@Operation(summary = "사용자 역할 수정", security = {@SecurityRequirement(name = "BearerAuth")})
50+
@PatchMapping("/{userId}/role")
51+
@PreAuthorize("hasAnyRole('LEAD','ORGANIZER','ADMIN') or T(inha.gdgoc.domain.user.enums.TeamType).HR == principal.team")
52+
public ResponseEntity<ApiResponse<Void, Void>> updateUserRole(@AuthenticationPrincipal CustomUserDetails me, @PathVariable Long userId, @RequestBody @Valid UpdateRoleRequest req) {
53+
userAdminService.updateUserRoleWithRules(me, userId, req.role());
54+
return ResponseEntity.ok(ApiResponse.ok("USER_ROLE_UPDATED"));
55+
}
56+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package inha.gdgoc.domain.user.dto.request;
2+
3+
import inha.gdgoc.domain.user.enums.UserRole;
4+
import jakarta.validation.constraints.NotNull;
5+
6+
public record UpdateRoleRequest(
7+
@NotNull UserRole role
8+
) {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package inha.gdgoc.domain.user.dto.request;
2+
3+
import inha.gdgoc.domain.user.enums.TeamType;
4+
import inha.gdgoc.domain.user.enums.UserRole;
5+
6+
public record UpdateUserRoleTeamRequest(
7+
UserRole role, // null 이면 변경 안 함
8+
TeamType team // null 이면 변경 안 함
9+
) {}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package inha.gdgoc.domain.user.dto.response;
2+
3+
import inha.gdgoc.domain.user.enums.TeamType;
4+
import inha.gdgoc.domain.user.enums.UserRole;
5+
6+
public record UserSummaryResponse(
7+
Long id,
8+
String name,
9+
String major,
10+
String studentId,
11+
String email,
12+
UserRole userRole,
13+
TeamType team
14+
) {}

src/main/java/inha/gdgoc/domain/user/entity/User.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,6 @@ public void updatePassword(String password) throws NoSuchAlgorithmException, Inv
131131
public boolean isGuest() {
132132
return this.userRole == UserRole.GUEST;
133133
}
134+
public void changeRole(UserRole role) { this.userRole = role; }
135+
public void changeTeam(TeamType team) { this.team = team; }
134136
}

src/main/java/inha/gdgoc/domain/user/repository/UserRepository.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package inha.gdgoc.domain.user.repository;
22

3+
import inha.gdgoc.domain.user.dto.response.UserSummaryResponse;
34
import inha.gdgoc.domain.user.entity.User;
45
import inha.gdgoc.domain.user.enums.TeamType;
56
import inha.gdgoc.domain.user.enums.UserRole;
7+
import org.jetbrains.annotations.NotNull;
8+
import org.springframework.data.domain.Page;
9+
import org.springframework.data.domain.Pageable;
610
import org.springframework.data.jpa.repository.JpaRepository;
11+
import org.springframework.data.jpa.repository.Query;
12+
import org.springframework.data.repository.query.Param;
713
import org.springframework.stereotype.Repository;
814

915
import java.util.Collection;
@@ -29,4 +35,15 @@ public interface UserRepository extends JpaRepository<User, Long>, UserRepositor
2935

3036
// 필요 시: 특정 팀 전체 멤버(역할 무관)
3137
List<User> findByTeam(TeamType team);
38+
39+
@Query("""
40+
select new inha.gdgoc.domain.user.dto.response.UserSummaryResponse(
41+
u.id, u.name, u.major, u.studentId, u.email, u.userRole, u.team
42+
)
43+
from User u
44+
where (:q is null or :q = '' or u.name like concat('%', :q, '%'))
45+
""")
46+
Page<UserSummaryResponse> findSummaries(@Param("q") String q, Pageable pageable);
47+
48+
@NotNull Optional<User> findById(@NotNull Long id);
3249
}

0 commit comments

Comments
 (0)