Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/main/java/side/onetime/auth/dto/CustomAdminDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package side.onetime.auth.dto;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import side.onetime.domain.AdminUser;
import side.onetime.domain.enums.AdminStatus;

import java.util.Collection;
import java.util.Collections;

public record CustomAdminDetails(AdminUser admin) implements UserDetails {

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (admin.getAdminStatus() == AdminStatus.PENDING_APPROVAL) {
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_PENDING_APPROVAL"));
}

return Collections.singletonList(new SimpleGrantedAuthority("ROLE_ADMIN"));
}

@Override
public String getPassword() {
return admin.getPassword();
}

@Override
public String getUsername() {
return admin.getName();
}

public Long getId() {
return admin.getId();
}
}
24 changes: 3 additions & 21 deletions src/main/java/side/onetime/auth/dto/CustomUserDetails.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package side.onetime.auth.dto;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import side.onetime.domain.User;

import java.util.Collection;
import java.util.List;

public record CustomUserDetails(User user) implements UserDetails {

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}

@Override
Expand All @@ -23,26 +25,6 @@ public String getUsername() {
return user.getName();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}

public Long getId() {
return user.getId();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package side.onetime.auth.exception;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import side.onetime.global.common.status.ErrorStatus;

import java.io.IOException;

@Slf4j
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

/**
* 접근할 수 있는 권한(Role)이 없을 때 발생한 accessDeniedException을 클라이언트에게 응답 형태로 반환합니다.
*
* @param request HTTP 요청 객체
* @param response HTTP 응답 객체
* @param accessDeniedException 접근 권한이 없어 발생한 exception
* @throws IOException 출력 스트림 처리 중 오류 발생 시
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = (authentication != null) ? authentication.getName() : "ANONYMOUS";
log.error("❌ 금지된 접근 - 사용자: {}, 요청 URI: {}, 메서드: {}", username, request.getRequestURI(), request.getMethod());

ErrorStatus status = ErrorStatus._FORBIDDEN;

response.setStatus(status.getHttpStatus().value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{"
+ "\"is_success\": false,"
+ "\"code\": \"" + status.getCode() + "\","
+ "\"message\": \"" + status.getMessage() + "\","
+ "\"payload\": null"
+ "}"
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package side.onetime.auth.exception;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import side.onetime.global.common.status.ErrorStatus;

import java.io.IOException;

@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

/**
* 로그인을 하지 않고 접근할 떄 발생한 authException을 클라이언트에게 응답 형태로 반환합니다.
*
* @param request HTTP 요청 객체
* @param response HTTP 응답 객체
* @param authException 로그인 없이 로그인이 필요한 리소스에 접근하여 발생한 exception
* @throws IOException 출력 스트림 처리 중 오류 발생 시
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.error("❌ 인증되지 않은 접근 - 요청 URI: {}, 메서드: {}", request.getRequestURI(), request.getMethod());

ErrorStatus status = ErrorStatus._UNAUTHORIZED;

response.setStatus(status.getHttpStatus().value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{"
+ "\"is_success\": false,"
+ "\"code\": \"" + status.getCode() + "\","
+ "\"message\": \"" + status.getMessage() + "\","
+ "\"payload\": null"
+ "}"
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package side.onetime.auth.service;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import side.onetime.auth.dto.CustomAdminDetails;
import side.onetime.domain.AdminUser;
import side.onetime.exception.CustomException;
import side.onetime.exception.status.AdminErrorStatus;
import side.onetime.repository.AdminRepository;

@Service
@RequiredArgsConstructor
public class CustomAdminDetailsService implements UserDetailsService {

private final AdminRepository adminRepository;

/**
* 관리자 이름으로 관리자 정보를 로드합니다.
*
* 데이터베이스에서 주어진 관리자 이름(username)을 기반으로 관리자를 조회하고,
* CustomAdminDetails 객체로 래핑하여 반환합니다.
*
* @param username 관리자 이름
* @return 관리자 상세 정보 (CustomAdminDetails 객체)
* @throws CustomException 관리자 이름에 해당하는 관리자가 없을 경우 예외를 발생시킵니다.
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AdminUser admin = adminRepository.findByName(username)
.orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER));
return new CustomAdminDetails(admin);
}

/**
* 관리자 ID로 관리자 정보를 로드합니다.
*
* 데이터베이스에서 주어진 관리자 ID를 기반으로 관리자를 조회하고,
* CustomAdminDetails 객체로 래핑하여 반환합니다.
*
* @param adminId 관리자 ID
* @return 관리자 상세 정보 (CustomAdminDetails 객체)
* @throws CustomException 관리자 ID에 해당하는 관리자가 없을 경우 예외를 발생시킵니다.
*/
public UserDetails loadAdminByAdminId(Long adminId) throws UsernameNotFoundException {
AdminUser admin = adminRepository.findById(adminId)
.orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER));
return new CustomAdminDetails(admin);
}
}
30 changes: 9 additions & 21 deletions src/main/java/side/onetime/controller/AdminController.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,12 @@ public ResponseEntity<ApiResponse<LoginAdminUserResponse>> loginAdminUser(
* 요청 헤더에 포함된 액세스 토큰을 기반으로 로그인된 관리자 정보를 조회합니다.
* 유효한 토큰이 아닐 경우 예외가 발생하며, 유효한 경우 이름, 이메일 정보를 반환합니다.
*
* @param authorizationHeader Authorization 헤더에 포함된 액세스 토큰
* @return 관리자 프로필 정보가 포함된 응답 객체
*/
@GetMapping("/profile")
public ResponseEntity<ApiResponse<GetAdminUserProfileResponse>> getAdminUserProfile(
@RequestHeader("Authorization") String authorizationHeader) {
public ResponseEntity<ApiResponse<GetAdminUserProfileResponse>> getAdminUserProfile() {

GetAdminUserProfileResponse response = adminService.getAdminUserProfile(authorizationHeader);
GetAdminUserProfileResponse response = adminService.getAdminUserProfile();
return ApiResponse.onSuccess(SuccessStatus._GET_ADMIN_USER_PROFILE, response);
}

Expand All @@ -83,14 +81,12 @@ public ResponseEntity<ApiResponse<GetAdminUserProfileResponse>> getAdminUserProf
*
* 마스터 관리자가 아닐 경우 예외가 발생하며, 유효한 경우 모든 관리자 이름, 이메일, 상태 정보가 포함됩니다.
*
* @param authorizationHeader Authorization 헤더에 포함된 액세스 토큰
* @return 전체 관리자 프로필 목록이 포함된 응답 객체
*/
@GetMapping("/all")
public ResponseEntity<ApiResponse<List<AdminUserDetailResponse>>> getAllAdminUserDetail(
@RequestHeader("Authorization") String authorizationHeader) {
public ResponseEntity<ApiResponse<List<AdminUserDetailResponse>>> getAllAdminUserDetail() {

List<AdminUserDetailResponse> response = adminService.getAllAdminUserDetail(authorizationHeader);
List<AdminUserDetailResponse> response = adminService.getAllAdminUserDetail();
return ApiResponse.onSuccess(SuccessStatus._GET_ALL_ADMIN_USER_DETAIL, response);
}

Expand All @@ -101,16 +97,14 @@ public ResponseEntity<ApiResponse<List<AdminUserDetailResponse>>> getAllAdminUse
* 요청된 관리자 ID와 수정할 권한 상태를 바탕으로 권한을 변경하며,
* 요청한 사용자가 마스터 관리자가 아닐 경우 예외가 발생합니다.
*
* @param authorizationHeader 요청자의 액세스 토큰
* @param request 수정할 관리자 ID와 변경할 권한 상태를 담은 요청 객체
* @return 성공 응답 메시지
*/
@PatchMapping("/status")
public ResponseEntity<ApiResponse<SuccessStatus>> updateAdminUserStatus(
@RequestHeader("Authorization") String authorizationHeader,
@Valid @RequestBody UpdateAdminUserStatusRequest request) {

adminService.updateAdminUserStatus(authorizationHeader, request);
adminService.updateAdminUserStatus(request);
return ApiResponse.onSuccess(SuccessStatus._UPDATE_ADMIN_USER_STATUS);
}

Expand All @@ -120,14 +114,12 @@ public ResponseEntity<ApiResponse<SuccessStatus>> updateAdminUserStatus(
* Authorization 헤더에 포함된 액세스 토큰을 통해 인증된 관리자 계정을 삭제합니다.
* - 토큰에 포함된 ID로 관리자 정보를 조회하여 삭제합니다.
*
* @param authorizationHeader Authorization 헤더에 포함된 액세스 토큰
* @return 성공 응답 메시지
*/
@PostMapping("/withdraw")
public ResponseEntity<ApiResponse<SuccessStatus>> withdrawAdminUser(
@RequestHeader("Authorization") String authorizationHeader) {
public ResponseEntity<ApiResponse<SuccessStatus>> withdrawAdminUser() {

adminService.withdrawAdminUser(authorizationHeader);
adminService.withdrawAdminUser();
return ApiResponse.onSuccess(SuccessStatus._WITHDRAW_ADMIN_USER);
}

Expand All @@ -137,21 +129,19 @@ public ResponseEntity<ApiResponse<SuccessStatus>> withdrawAdminUser(
* 정렬 기준으로는 created_date, end_time, participant_count 등이 가능하며,
* 응답은 최대 20개씩 페이지 단위로 제공됩니다.
*
* @param authorizationHeader Authorization 헤더 (Bearer 토큰)
* @param page 조회할 페이지 번호 (1부터 시작)
* @param keyword 정렬 기준 필드명 (예: "created_date", "end_time", "participant_count")
* @param sorting 정렬 방향 ("asc" 또는 "desc")
* @return 이벤트 목록 및 페이지 정보가 포함된 응답 DTO
*/
@GetMapping("/dashboard/events")
public ResponseEntity<ApiResponse<GetAllDashboardEventsResponse>> getAllDashboardEvents(
@RequestHeader("Authorization") String authorizationHeader,
@RequestParam(value = "page", defaultValue = "1") @Min(1) int page,
@RequestParam(value = "keyword", defaultValue = "created_date") String keyword,
@RequestParam(value = "sorting", defaultValue = "desc") String sorting
) {
Pageable pageable = PageRequest.of(page - 1, 20);
GetAllDashboardEventsResponse response = adminService.getAllDashboardEvents(authorizationHeader, pageable, keyword, sorting);
GetAllDashboardEventsResponse response = adminService.getAllDashboardEvents(pageable, keyword, sorting);
return ApiResponse.onSuccess(SuccessStatus._GET_ALL_DASHBOARD_EVENTS, response);
}

Expand All @@ -161,21 +151,19 @@ public ResponseEntity<ApiResponse<GetAllDashboardEventsResponse>> getAllDashboar
* 정렬 기준으로는 name, email, created_date, participation_count 등이 가능하며,
* 응답은 최대 20개씩 페이지 단위로 제공됩니다.
*
* @param authorizationHeader Authorization 헤더 (Bearer 토큰)
* @param page 조회할 페이지 번호 (1부터 시작)
* @param keyword 정렬 기준 필드명 (예: "name", "email", "created_date", "participation_count")
* @param sorting 정렬 방향 ("asc" 또는 "desc")
* @return 사용자 목록 및 페이지 정보가 포함된 응답 DTO
*/
@GetMapping("/dashboard/users")
public ResponseEntity<ApiResponse<GetAllDashboardUsersResponse>> getAllDashboardUsers(
@RequestHeader("Authorization") String authorizationHeader,
@RequestParam(value = "page", defaultValue = "1") @Min(1) int page,
@RequestParam(value = "keyword", defaultValue = "created_date") String keyword,
@RequestParam(value = "sorting", defaultValue = "desc") String sorting
) {
Pageable pageable = PageRequest.of(page - 1, 20);
GetAllDashboardUsersResponse response = adminService.getAllDashboardUsers(authorizationHeader, pageable, keyword, sorting);
GetAllDashboardUsersResponse response = adminService.getAllDashboardUsers(pageable, keyword, sorting);
return ApiResponse.onSuccess(SuccessStatus._GET_ALL_DASHBOARD_USERS, response);
}
}
Loading
Loading