diff --git a/src/main/java/sopt/org/hmh/domain/admin/controller/AdminApi.java b/src/main/java/sopt/org/hmh/domain/admin/controller/AdminApi.java new file mode 100644 index 00000000..51b57e3f --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/controller/AdminApi.java @@ -0,0 +1,24 @@ +package sopt.org.hmh.domain.admin.controller; + +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.http.ResponseEntity; +import sopt.org.hmh.domain.admin.dto.request.AdminDailyChallengeRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminLoginRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminUserIdRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminUserInfoRequest; +import sopt.org.hmh.domain.admin.dto.response.AdminTokenResponse; +import sopt.org.hmh.global.common.response.BaseResponse; + +public interface AdminApi { + @Operation(summary = "관리자 로그인") + ResponseEntity> orderAdminLogin(AdminLoginRequest request); + + @Operation(summary = "관리자 권한으로 유저 즉시 삭제") + ResponseEntity orderAdminWithdrawImmediately(AdminUserIdRequest request); + + @Operation(summary = "관리자 권한으로 유저 정보 변경") + ResponseEntity orderAdminChangeUserInfo(AdminUserInfoRequest request); + + @Operation(summary = "관리자 권한으로 유저 챌린지 정보 변경") + ResponseEntity orderAdminChangeDailyChallengeInfo(AdminDailyChallengeRequest request); +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/controller/AdminController.java b/src/main/java/sopt/org/hmh/domain/admin/controller/AdminController.java new file mode 100644 index 00000000..647cea23 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/controller/AdminController.java @@ -0,0 +1,67 @@ +package sopt.org.hmh.domain.admin.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import sopt.org.hmh.domain.admin.dto.request.AdminDailyChallengeRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminLoginRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminUserIdRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminUserInfoRequest; +import sopt.org.hmh.domain.admin.dto.response.AdminTokenResponse; +import sopt.org.hmh.domain.admin.exception.AdminSuccess; +import sopt.org.hmh.domain.admin.service.AdminFacade; +import sopt.org.hmh.global.common.response.BaseResponse; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/admin") +public class AdminController implements AdminApi { + + private final AdminFacade adminFacade; + + @Override + @PostMapping("/login") + public ResponseEntity> orderAdminLogin( + @RequestBody final AdminLoginRequest request) { + return ResponseEntity + .status(AdminSuccess.ADMIN_LOGIN_SUCCESS.getHttpStatus()) + .body(BaseResponse.success(AdminSuccess.ADMIN_LOGIN_SUCCESS, + adminFacade.adminLogin(request.authCode()))); + } + + @Override + @DeleteMapping("/user") + public ResponseEntity orderAdminWithdrawImmediately( + @RequestBody @Valid final AdminUserIdRequest request) { + adminFacade.withdrawImmediately(request.userId()); + return ResponseEntity + .noContent() + .build(); + } + + @Override + @PatchMapping("/user") + public ResponseEntity orderAdminChangeUserInfo( + @RequestBody @Valid final AdminUserInfoRequest request) { + adminFacade.changeUserInfo(request); + return ResponseEntity + .noContent() + .build(); + } + + @Override + @PatchMapping("/challenge/daily") + public ResponseEntity orderAdminChangeDailyChallengeInfo( + @RequestBody @Valid final AdminDailyChallengeRequest request) { + adminFacade.changeDailyChallengeInfo(request); + return ResponseEntity + .noContent() + .build(); + } +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminDailyChallengeRequest.java b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminDailyChallengeRequest.java new file mode 100644 index 00000000..ccd4e389 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminDailyChallengeRequest.java @@ -0,0 +1,12 @@ +package sopt.org.hmh.domain.admin.dto.request; + +import java.time.LocalDate; +import java.util.List; +import sopt.org.hmh.domain.dailychallenge.domain.Status; + +public record AdminDailyChallengeRequest( + Long userId, + LocalDate startDate, + List statuses +) { +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminLoginRequest.java b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminLoginRequest.java new file mode 100644 index 00000000..a280bdab --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminLoginRequest.java @@ -0,0 +1,7 @@ +package sopt.org.hmh.domain.admin.dto.request; + +public record AdminLoginRequest( + String authCode +) { + +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminUserIdRequest.java b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminUserIdRequest.java new file mode 100644 index 00000000..c1d513c3 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminUserIdRequest.java @@ -0,0 +1,7 @@ +package sopt.org.hmh.domain.admin.dto.request; + +public record AdminUserIdRequest( + Long userId +) { + +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminUserInfoRequest.java b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminUserInfoRequest.java new file mode 100644 index 00000000..ab7c2969 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/dto/request/AdminUserInfoRequest.java @@ -0,0 +1,9 @@ +package sopt.org.hmh.domain.admin.dto.request; + +public record AdminUserInfoRequest( + Long userId, + String name, + Integer point +) { + +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/dto/response/AdminTokenResponse.java b/src/main/java/sopt/org/hmh/domain/admin/dto/response/AdminTokenResponse.java new file mode 100644 index 00000000..8c6458a1 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/dto/response/AdminTokenResponse.java @@ -0,0 +1,6 @@ +package sopt.org.hmh.domain.admin.dto.response; + +public record AdminTokenResponse( + String accessToken +) { +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/exception/AdminError.java b/src/main/java/sopt/org/hmh/domain/admin/exception/AdminError.java new file mode 100644 index 00000000..61bdcb71 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/exception/AdminError.java @@ -0,0 +1,31 @@ +package sopt.org.hmh.domain.admin.exception; + +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import sopt.org.hmh.global.common.exception.base.ErrorBase; + +@AllArgsConstructor +public enum AdminError implements ErrorBase { + + // 401 UNAUTHORIZED + INVALID_ADMIN_AUTH_CODE(HttpStatus.UNAUTHORIZED, "관리자 인증 번호가 일치하지 않습니다."), + ; + + private final HttpStatus status; + private final String errorMessage; + + @Override + public int getHttpStatusCode() { + return this.status.value(); + } + + @Override + public HttpStatus getHttpStatus() { + return this.status; + } + + @Override + public String getErrorMessage() { + return this.errorMessage; + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/admin/exception/AdminException.java b/src/main/java/sopt/org/hmh/domain/admin/exception/AdminException.java new file mode 100644 index 00000000..7c0ecfd8 --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/exception/AdminException.java @@ -0,0 +1,10 @@ +package sopt.org.hmh.domain.admin.exception; + +import sopt.org.hmh.global.common.exception.base.ExceptionBase; + +public class AdminException extends ExceptionBase { + + public AdminException(AdminError errorBase) { + super(errorBase); + } +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/exception/AdminSuccess.java b/src/main/java/sopt/org/hmh/domain/admin/exception/AdminSuccess.java new file mode 100644 index 00000000..fc9ba43c --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/exception/AdminSuccess.java @@ -0,0 +1,34 @@ +package sopt.org.hmh.domain.admin.exception; + +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import sopt.org.hmh.global.common.exception.base.SuccessBase; + +@AllArgsConstructor +public enum AdminSuccess implements SuccessBase { + + // 200 OK + ADMIN_LOGIN_SUCCESS(HttpStatus.OK, "관리자 로그인에 성공했습니다."), + + // 204 NO CONTENT + ADMIN_WITHDRAW_IMMEDIATELY_SUCCESS(HttpStatus.NO_CONTENT, "관리자 권한으로 유저 즉시 삭제에 성공했습니다."), + ; + + private final HttpStatus status; + private final String successMessage; + + @Override + public int getHttpStatusCode() { + return this.status.value(); + } + + @Override + public HttpStatus getHttpStatus() { + return this.status; + } + + @Override + public String getSuccessMessage() { + return this.successMessage; + } +} diff --git a/src/main/java/sopt/org/hmh/domain/admin/service/AdminFacade.java b/src/main/java/sopt/org/hmh/domain/admin/service/AdminFacade.java new file mode 100644 index 00000000..6fbdc22d --- /dev/null +++ b/src/main/java/sopt/org/hmh/domain/admin/service/AdminFacade.java @@ -0,0 +1,81 @@ +package sopt.org.hmh.domain.admin.service; + +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sopt.org.hmh.domain.admin.dto.request.AdminDailyChallengeRequest; +import sopt.org.hmh.domain.admin.dto.request.AdminUserInfoRequest; +import sopt.org.hmh.domain.admin.dto.response.AdminTokenResponse; +import sopt.org.hmh.domain.admin.exception.AdminError; +import sopt.org.hmh.domain.admin.exception.AdminException; +import sopt.org.hmh.domain.challenge.domain.exception.ChallengeError; +import sopt.org.hmh.domain.challenge.domain.exception.ChallengeException; +import sopt.org.hmh.domain.challenge.service.ChallengeService; +import sopt.org.hmh.domain.dailychallenge.domain.Status; +import sopt.org.hmh.domain.dailychallenge.service.DailyChallengeService; +import sopt.org.hmh.domain.user.domain.User; +import sopt.org.hmh.domain.user.service.UserService; +import sopt.org.hmh.global.auth.jwt.TokenService; + +@Service +@RequiredArgsConstructor +public class AdminFacade { + + @Value("${jwt.admin-auth-code}") + private String adminAuthCode; + + private final UserService userService; + private final TokenService tokenService; + private final ChallengeService challengeService; + private final DailyChallengeService dailyChallengeService; + + public AdminTokenResponse adminLogin(String authCode) { + validateAdminAuthCode(authCode); + return new AdminTokenResponse(tokenService.issueAdminToken()); + } + + private void validateAdminAuthCode(String authCode) { + if (!adminAuthCode.equals(authCode)) { + throw new AdminException(AdminError.INVALID_ADMIN_AUTH_CODE); + } + } + + @Transactional + public void withdrawImmediately(Long userId) { + userService.checkIsExistUserId(userId); + challengeService.deleteChallengeRelatedByUserId(userId); + userService.withdrawImmediately(userId); + } + + @Transactional + public void changeUserInfo(AdminUserInfoRequest request) { + User user = userService.findByIdOrThrowException(request.userId()); + if (Objects.nonNull(request.point())) { + user.changePoint(request.point()); + } + if (Objects.nonNull(request.name())) { + user.changeName(request.name()); + } + } + + @Transactional + public void changeDailyChallengeInfo(AdminDailyChallengeRequest request) { + Long currentChallengeId = userService.getCurrentChallengeIdByUserId(request.userId()); + List statuses = request.statuses(); + LocalDate challengeDate = request.startDate(); + + validateStatusesPeriod(currentChallengeId, statuses); + dailyChallengeService.changeInfoOfDailyChallenges(currentChallengeId, statuses, challengeDate); + } + + private void validateStatusesPeriod(Long challengeId, List statuses) { + Integer challengePeriod = challengeService.getChallengePeriod(challengeId); + if (challengePeriod != statuses.size()) { + throw new ChallengeException(ChallengeError.INVALID_PERIOD_NUMERIC); + } + } +} diff --git a/src/main/java/sopt/org/hmh/domain/auth/service/AuthFacade.java b/src/main/java/sopt/org/hmh/domain/auth/service/AuthFacade.java index fbc2725a..5e314308 100644 --- a/src/main/java/sopt/org/hmh/domain/auth/service/AuthFacade.java +++ b/src/main/java/sopt/org/hmh/domain/auth/service/AuthFacade.java @@ -73,7 +73,7 @@ private LoginResponse performLogin(String socialAccessToken, SocialPlatform soci kakaoLoginService.updateUserInfoByKakao(loginUser, socialAccessToken); } Long userId = loginUser.getId(); - return new LoginResponse(userId, tokenService.issueToken(userId)); + return new LoginResponse(userId, tokenService.issueToken(userId.toString())); } public SocialAccessTokenResponse getSocialAccessTokenByAuthorizationCode(String code) { diff --git a/src/main/java/sopt/org/hmh/domain/challenge/controller/ChallengeController.java b/src/main/java/sopt/org/hmh/domain/challenge/controller/ChallengeController.java index 6dd9f363..3c503397 100644 --- a/src/main/java/sopt/org/hmh/domain/challenge/controller/ChallengeController.java +++ b/src/main/java/sopt/org/hmh/domain/challenge/controller/ChallengeController.java @@ -25,9 +25,10 @@ public class ChallengeController implements ChallengeApi { @PostMapping @Override - public ResponseEntity> orderAddChallenge(@UserId final Long userId, - @RequestHeader("OS") final String os, - @RequestBody @Valid final ChallengeRequest request) { + public ResponseEntity> orderAddChallenge( + @UserId final Long userId, + @RequestHeader("OS") final String os, + @RequestBody @Valid final ChallengeRequest request) { challengeFacade.addChallenge(userId, request, os); return ResponseEntity @@ -37,8 +38,9 @@ public ResponseEntity> orderAddChallenge(@UserId final Long user @GetMapping @Override - public ResponseEntity> orderGetChallenge(@UserId final Long userId, - @RequestHeader("OS") final String os) { + public ResponseEntity> orderGetChallenge( + @UserId final Long userId, + @RequestHeader("OS") final String os) { return ResponseEntity .status(ChallengeSuccess.GET_CHALLENGE_SUCCESS.getHttpStatus()) .body(BaseResponse.success(ChallengeSuccess.GET_CHALLENGE_SUCCESS, @@ -47,8 +49,9 @@ public ResponseEntity> orderGetChallenge(@UserId @GetMapping("/home") @Override - public ResponseEntity> orderGetDailyChallenge(@UserId final Long userId, - @RequestHeader("OS") final String os) { + public ResponseEntity> orderGetDailyChallenge( + @UserId final Long userId, + @RequestHeader("OS") final String os) { return ResponseEntity .status(ChallengeSuccess.GET_DAILY_CHALLENGE_SUCCESS.getHttpStatus()) .body(BaseResponse.success(ChallengeSuccess.GET_DAILY_CHALLENGE_SUCCESS, @@ -57,9 +60,10 @@ public ResponseEntity> orderGetDailyChallen @PostMapping("/app") @Override - public ResponseEntity> orderAddApps(@UserId final Long userId, - @RequestHeader("OS") final String os, - @RequestBody @Valid final ChallengeAppArrayRequest requests) { + public ResponseEntity> orderAddApps( + @UserId final Long userId, + @RequestHeader("OS") final String os, + @RequestBody @Valid final ChallengeAppArrayRequest requests) { challengeFacade.addAppsToCurrentChallenge(userId, requests.apps(), os); return ResponseEntity @@ -70,9 +74,10 @@ public ResponseEntity> orderAddApps(@UserId final Long userId, @DeleteMapping("/app") @Override - public ResponseEntity> orderRemoveApp(@UserId final Long userId, - @RequestHeader("OS") final String os, - @RequestBody @Valid final AppRemoveRequest request) { + public ResponseEntity> orderRemoveApp( + @UserId final Long userId, + @RequestHeader("OS") final String os, + @RequestBody @Valid final AppRemoveRequest request) { challengeFacade.removeAppFromCurrentChallenge(userId, request.appCode(), os); return ResponseEntity diff --git a/src/main/java/sopt/org/hmh/domain/challenge/repository/ChallengeRepository.java b/src/main/java/sopt/org/hmh/domain/challenge/repository/ChallengeRepository.java index b426c20f..1126f585 100644 --- a/src/main/java/sopt/org/hmh/domain/challenge/repository/ChallengeRepository.java +++ b/src/main/java/sopt/org/hmh/domain/challenge/repository/ChallengeRepository.java @@ -10,4 +10,6 @@ public interface ChallengeRepository extends JpaRepository { Optional findById(Long id); void deleteByUserIdIn(List userId); + + void deleteByUserId(Long userId); } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/challenge/service/ChallengeService.java b/src/main/java/sopt/org/hmh/domain/challenge/service/ChallengeService.java index 569a4684..ccf0cf02 100644 --- a/src/main/java/sopt/org/hmh/domain/challenge/service/ChallengeService.java +++ b/src/main/java/sopt/org/hmh/domain/challenge/service/ChallengeService.java @@ -15,10 +15,14 @@ public class ChallengeService { private final ChallengeRepository challengeRepository; - public void deleteChallengeRelatedByUserId(List expiredUserIdList) { + public void deleteChallengeRelatedByUserIds(List expiredUserIdList) { challengeRepository.deleteByUserIdIn(expiredUserIdList); } + public void deleteChallengeRelatedByUserId(Long userId) { + challengeRepository.deleteByUserId(userId); + } + public Challenge findByIdOrElseThrow(Long challengeId) { return challengeRepository.findById(challengeId).orElseThrow( () -> new ChallengeException(ChallengeError.CHALLENGE_NOT_FOUND)); @@ -31,4 +35,8 @@ public List getCurrentChallengeAppByChallengeId(Long challengeId) public Challenge save(Challenge challenge) { return challengeRepository.save(challenge); } + + public Integer getChallengePeriod(Long challengeId) { + return findByIdOrElseThrow(challengeId).getPeriod(); + } } diff --git a/src/main/java/sopt/org/hmh/domain/dailychallenge/domain/DailyChallenge.java b/src/main/java/sopt/org/hmh/domain/dailychallenge/domain/DailyChallenge.java index d62ac340..2d568237 100644 --- a/src/main/java/sopt/org/hmh/domain/dailychallenge/domain/DailyChallenge.java +++ b/src/main/java/sopt/org/hmh/domain/dailychallenge/domain/DailyChallenge.java @@ -9,7 +9,6 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.util.Assert; import sopt.org.hmh.domain.app.domain.HistoryApp; import sopt.org.hmh.global.common.domain.BaseTimeEntity; import sopt.org.hmh.domain.challenge.domain.Challenge; @@ -54,6 +53,10 @@ public class DailyChallenge extends BaseTimeEntity { this.status = Status.NONE; } + public void changeChallengeDate(LocalDate challengeDate) { + this.challengeDate = challengeDate; + } + public void changeStatus(Status status) { this.status = status; } diff --git a/src/main/java/sopt/org/hmh/domain/dailychallenge/repository/DailyChallengeRepository.java b/src/main/java/sopt/org/hmh/domain/dailychallenge/repository/DailyChallengeRepository.java index e08921fd..ca5bd335 100644 --- a/src/main/java/sopt/org/hmh/domain/dailychallenge/repository/DailyChallengeRepository.java +++ b/src/main/java/sopt/org/hmh/domain/dailychallenge/repository/DailyChallengeRepository.java @@ -1,10 +1,14 @@ package sopt.org.hmh.domain.dailychallenge.repository; import java.time.LocalDate; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import sopt.org.hmh.domain.dailychallenge.domain.DailyChallenge; public interface DailyChallengeRepository extends JpaRepository { + Optional findByChallengeDateAndUserId(LocalDate challengeDate, Long userId); + + List findAllByChallengeId(Long challengeId); } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeFacade.java b/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeFacade.java index 122b52fc..b4ba716e 100644 --- a/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeFacade.java +++ b/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeFacade.java @@ -25,10 +25,12 @@ public class DailyChallengeFacade { @Transactional public void addFinishedDailyChallengeHistory(Long userId, FinishedDailyChallengeListRequest requests, String os) { Long currentChallengeId = userService.getCurrentChallengeIdByUserId(userId); - List currentChallengeApps = challengeService.getCurrentChallengeAppByChallengeId(currentChallengeId); + List currentChallengeApps = + challengeService.getCurrentChallengeAppByChallengeId(currentChallengeId); requests.finishedDailyChallenges().forEach(request -> { - DailyChallenge dailyChallenge = dailyChallengeService.findByChallengeDateAndUserIdOrThrowException(request.challengeDate(), userId); + DailyChallenge dailyChallenge = + dailyChallengeService.findByChallengeDateAndUserIdOrThrowException(request.challengeDate(), userId); dailyChallengeService.changeStatusByCurrentStatus(dailyChallenge); historyAppService.addHistoryApp(currentChallengeApps, request.apps(), dailyChallenge, os); }); @@ -37,12 +39,14 @@ public void addFinishedDailyChallengeHistory(Long userId, FinishedDailyChallenge @Transactional public void changeDailyChallengeStatusByIsSuccess(Long userId, FinishedDailyChallengeStatusListRequest requests) { requests.finishedDailyChallenges().forEach(request -> { - DailyChallenge dailyChallenge = dailyChallengeService.findByChallengeDateAndUserIdOrThrowException(request.challengeDate(), userId); + DailyChallenge dailyChallenge = + dailyChallengeService.findByChallengeDateAndUserIdOrThrowException(request.challengeDate(), userId); if (request.isSuccess()) { dailyChallengeService.validateDailyChallengeStatus(dailyChallenge.getStatus(), List.of(Status.NONE)); dailyChallenge.changeStatus(Status.UNEARNED); } else { - dailyChallengeService.validateDailyChallengeStatus(dailyChallenge.getStatus(), List.of(Status.NONE, Status.FAILURE)); + dailyChallengeService.validateDailyChallengeStatus( + dailyChallenge.getStatus(), List.of(Status.NONE, Status.FAILURE)); dailyChallenge.changeStatus(Status.FAILURE); } }); diff --git a/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeService.java b/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeService.java index 7062cae1..c5ee346e 100644 --- a/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeService.java +++ b/src/main/java/sopt/org/hmh/domain/dailychallenge/service/DailyChallengeService.java @@ -53,4 +53,26 @@ public void addDailyChallenge(Long userId, LocalDate startDate, Challenge challe .goalTime(challenge.getGoalTime()).build()) .toList()); } + + public List getDailyChallengesByChallengeId(Long challengeId) { + return dailyChallengeRepository.findAllByChallengeId(challengeId); + } + + public void changeInfoOfDailyChallenges(Long challengeId, List statuses, LocalDate challengeDate) { + List dailyChallenges = getDailyChallengesByChallengeId(challengeId); + changeStatusOfDailyChallenges(dailyChallenges, statuses); + changeChallengeDateOfDailyChallenges(dailyChallenges, challengeDate); + } + + private void changeStatusOfDailyChallenges(List dailyChallenges, List statuses) { + for (int i = 0; i < dailyChallenges.size(); i++) { + dailyChallenges.get(i).changeStatus(statuses.get(i)); + } + } + + private void changeChallengeDateOfDailyChallenges(List dailyChallenges, LocalDate challengeDate) { + for (int i = 0; i < dailyChallenges.size(); i++) { + dailyChallenges.get(i).changeChallengeDate(challengeDate.plusDays(i)); + } + } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/domain/user/domain/User.java b/src/main/java/sopt/org/hmh/domain/user/domain/User.java index 0ed66780..fb831734 100644 --- a/src/main/java/sopt/org/hmh/domain/user/domain/User.java +++ b/src/main/java/sopt/org/hmh/domain/user/domain/User.java @@ -89,6 +89,14 @@ public Integer increasePoint(Integer earnedPoint) { return this.point; } + public void changePoint(Integer point) { + this.point = point; + } + + public void changeName(String name) { + this.name = name; + } + public void changeCurrentChallengeId(Long currentChallengeId) { this.currentChallengeId = currentChallengeId; } diff --git a/src/main/java/sopt/org/hmh/domain/user/service/ExpiredUserDeleteScheduler.java b/src/main/java/sopt/org/hmh/domain/user/service/ExpiredUserDeleteScheduler.java index 055fa30f..e3f096f8 100644 --- a/src/main/java/sopt/org/hmh/domain/user/service/ExpiredUserDeleteScheduler.java +++ b/src/main/java/sopt/org/hmh/domain/user/service/ExpiredUserDeleteScheduler.java @@ -25,6 +25,6 @@ public void deleteExpiredUser() { public void deleteExpiredUser(LocalDateTime currentDate) { List expiredUserList = userRepository.findIdByDeletedAtBeforeAndIsDeletedIsTrue(currentDate); userRepository.deleteAllById(expiredUserList); - challengeService.deleteChallengeRelatedByUserId(expiredUserList); + challengeService.deleteChallengeRelatedByUserIds(expiredUserList); } } diff --git a/src/main/java/sopt/org/hmh/domain/user/service/UserService.java b/src/main/java/sopt/org/hmh/domain/user/service/UserService.java index 12e2bfa9..397c5252 100644 --- a/src/main/java/sopt/org/hmh/domain/user/service/UserService.java +++ b/src/main/java/sopt/org/hmh/domain/user/service/UserService.java @@ -85,6 +85,16 @@ public User findByIdOrThrowException(Long userId) { () -> new UserException(UserError.NOT_FOUND_USER)); } + private boolean isExistUserId(Long userId) { + return userRepository.existsById(userId); + } + + public void checkIsExistUserId(Long userId) { + if (!isExistUserId(userId)) { + throw new UserException(UserError.NOT_FOUND_USER); + } + } + public Long getCurrentChallengeIdByUserId(Long userId) { return Optional.ofNullable(this.findByIdOrThrowException(userId).getCurrentChallengeId()) .orElseThrow(() -> new UserException(UserError.NOT_FOUND_CURRENT_CHALLENGE_ID)); @@ -100,4 +110,8 @@ public IsLockTodayResponse checkIsTodayLock(Long userId, LocalDate lockCheckDate LocalDate userRecentLockDate = this.findByIdOrThrowException(userId).getRecentLockDate(); return new IsLockTodayResponse(lockCheckDate.equals(userRecentLockDate)); } + + public void withdrawImmediately(Long userId) { + userRepository.deleteById(userId); + } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/UserIdArgumentResolver.java b/src/main/java/sopt/org/hmh/global/auth/UserIdArgumentResolver.java index 87c5dd1e..0318b423 100644 --- a/src/main/java/sopt/org/hmh/global/auth/UserIdArgumentResolver.java +++ b/src/main/java/sopt/org/hmh/global/auth/UserIdArgumentResolver.java @@ -21,9 +21,11 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - return SecurityContextHolder.getContext() + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + return Long.parseLong(SecurityContextHolder.getContext() .getAuthentication() - .getPrincipal(); + .getPrincipal() + .toString()); } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtConstants.java b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtConstants.java index 792d2ee9..4e5ee2b8 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtConstants.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtConstants.java @@ -8,4 +8,5 @@ public abstract class JwtConstants { public static final String AUTHORIZATION = "Authorization"; public static final String BEARER = "Bearer "; public static final String CHARACTER_ENCODING = "UTF-8"; + public static final String ADMIN_ROLE = "ADMIN"; } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtGenerator.java b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtGenerator.java index 5da40648..f30eba8e 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtGenerator.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtGenerator.java @@ -1,5 +1,7 @@ package sopt.org.hmh.global.auth.jwt; +import static sopt.org.hmh.global.auth.jwt.JwtConstants.ADMIN_ROLE; + import io.jsonwebtoken.Header; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; @@ -22,20 +24,34 @@ public class JwtGenerator { private Long ACCESS_TOKEN_EXPIRATION_TIME; @Value("${jwt.refresh-token-expiration-time}") private Long REFRESH_TOKEN_EXPIRATION_TIME; + @Value("${jwt.admin-access-token-expiration-time}") + private Long ADMIN_ACCESS_TOKEN_EXPIRATION_TIME; - public String generateToken(Long userId, boolean isRefreshToken) { + public String generateToken(String subjectId, boolean isRefreshToken) { final Date now = generateNowDate(); final Date expiration = generateExpirationDate(isRefreshToken, now); return Jwts.builder() .setHeaderParam(Header.TYPE, Header.JWT_TYPE) - .setSubject(String.valueOf(userId)) + .setSubject(subjectId) .setIssuedAt(now) .setExpiration(expiration) .signWith(getSigningKey()) .compact(); } + public String generateAdminToken() { + final Date now = generateNowDate(); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setSubject(ADMIN_ROLE) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + ADMIN_ACCESS_TOKEN_EXPIRATION_TIME)) + .signWith(getSigningKey()) + .compact(); + } + public JwtParser getJwtParser() { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtPrefixExtractor.java b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtPrefixExtractor.java new file mode 100644 index 00000000..bf825e6b --- /dev/null +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtPrefixExtractor.java @@ -0,0 +1,19 @@ +package sopt.org.hmh.global.auth.jwt; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.util.StringUtils; +import sopt.org.hmh.global.auth.jwt.exception.JwtError; +import sopt.org.hmh.global.auth.jwt.exception.JwtException; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JwtPrefixExtractor { + + public static String extractPrefix(String accessToken) { + if (StringUtils.hasText(accessToken) && accessToken.startsWith(JwtConstants.BEARER)) { + return accessToken.substring(JwtConstants.BEARER.length()); + } + throw new JwtException(JwtError.INVALID_ACCESS_TOKEN); + } + +} diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtProvider.java b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtProvider.java index a1933e0f..ade8b1df 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtProvider.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtProvider.java @@ -1,5 +1,7 @@ package sopt.org.hmh.global.auth.jwt; +import static sopt.org.hmh.global.auth.jwt.JwtPrefixExtractor.extractPrefix; + import io.jsonwebtoken.JwtParser; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -10,15 +12,20 @@ public class JwtProvider { private final JwtGenerator jwtGenerator; - public TokenResponse issueToken(Long userId) { - return new TokenResponse(jwtGenerator.generateToken(userId, false), - jwtGenerator.generateToken(userId, true)); + public TokenResponse issueToken(String subjectId) { + return new TokenResponse(jwtGenerator.generateToken(subjectId, false), + jwtGenerator.generateToken(subjectId, true)); } - public Long getSubject(String token) { + public String getSubject(String token) { + String extractedToken = extractPrefix(token); JwtParser jwtParser = jwtGenerator.getJwtParser(); - return Long.valueOf(jwtParser.parseClaimsJws(token) + return jwtParser.parseClaimsJws(extractedToken) .getBody() - .getSubject()); + .getSubject(); + } + + public String issueAdminToken() { + return jwtGenerator.generateAdminToken(); } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtValidator.java b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtValidator.java index b8ed8b24..c964c791 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/JwtValidator.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/JwtValidator.java @@ -1,6 +1,11 @@ package sopt.org.hmh.global.auth.jwt; +import static sopt.org.hmh.global.auth.jwt.JwtConstants.ADMIN_ROLE; +import static sopt.org.hmh.global.auth.jwt.JwtPrefixExtractor.extractPrefix; + +import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtParser; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -32,8 +37,16 @@ public void validateRefreshToken(String refreshToken) { } } - private void parseToken(String token) { + private Jws parseToken(String token) { + String extractedToken = extractPrefix(token); JwtParser jwtParser = jwtGenerator.getJwtParser(); - jwtParser.parseClaimsJws(token); + return jwtParser.parseClaimsJws(extractedToken); + } + + public void validateAdminToken(String token) { + String subject = parseToken(token).getBody().getSubject(); + if (!subject.equals(ADMIN_ROLE)) { + throw new JwtException(JwtError.INVALID_ADMIN_TOKEN); + } } } \ No newline at end of file diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/TokenService.java b/src/main/java/sopt/org/hmh/global/auth/jwt/TokenService.java index 64e2f6b9..b589abac 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/TokenService.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/TokenService.java @@ -1,11 +1,11 @@ package sopt.org.hmh.global.auth.jwt; +import static sopt.org.hmh.global.auth.jwt.JwtPrefixExtractor.extractPrefix; + import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import sopt.org.hmh.domain.auth.dto.response.ReissueResponse; -import sopt.org.hmh.global.auth.jwt.exception.JwtError; -import sopt.org.hmh.global.auth.jwt.exception.JwtException; @Service @RequiredArgsConstructor @@ -16,22 +16,22 @@ public class TokenService { @Transactional public ReissueResponse reissueToken(String refreshToken) { - String parsedRefreshToken = parseTokenString(refreshToken); - Long userId = jwtProvider.getSubject(parsedRefreshToken); + String parsedRefreshToken = extractPrefix(refreshToken); + String userId = jwtProvider.getSubject(parsedRefreshToken); jwtValidator.validateRefreshToken(parsedRefreshToken); return ReissueResponse.of(jwtProvider.issueToken(userId)); } - private String parseTokenString(String tokenString) { - String[] parsedTokens = tokenString.split(" "); - if (parsedTokens.length != 2) { - throw new JwtException(JwtError.INVALID_TOKEN_HEADER); - } - return parsedTokens[1]; + public TokenResponse issueToken(String subjectId) { + return jwtProvider.issueToken(subjectId); + } + + public String issueAdminToken() { + return jwtProvider.issueAdminToken(); } - public TokenResponse issueToken(Long userId) { - return jwtProvider.issueToken(userId); + public void validateAdminToken(String token){ + jwtValidator.validateAdminToken(token); } } diff --git a/src/main/java/sopt/org/hmh/global/auth/jwt/exception/JwtError.java b/src/main/java/sopt/org/hmh/global/auth/jwt/exception/JwtError.java index 845d98a6..9006d88c 100644 --- a/src/main/java/sopt/org/hmh/global/auth/jwt/exception/JwtError.java +++ b/src/main/java/sopt/org/hmh/global/auth/jwt/exception/JwtError.java @@ -26,11 +26,14 @@ public enum JwtError implements ErrorBase { INVALID_IDENTITY_TOKEN_CLAIMS(HttpStatus.UNAUTHORIZED, "유효하지 않은 애플 아이덴티티 토큰 클레임입니다."), UNABLE_TO_CREATE_APPLE_PUBLIC_KEY(HttpStatus.UNAUTHORIZED, "애플 로그인 중 퍼블릭 키 생성에 문제가 발생했습니다."), + INVALID_ADMIN_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 관리자 액세스 토큰입니다."), + // 404 NOT FOUND NOT_FOUND_REFRESH_TOKEN_ERROR(HttpStatus.NOT_FOUND, "존재하지 않는 리프레시 토큰입니다."), // 500 INTERNAL ERROR INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다."); + private final HttpStatus status; private final String errorMessage; diff --git a/src/main/java/sopt/org/hmh/global/auth/security/JwtAuthenticationFilter.java b/src/main/java/sopt/org/hmh/global/auth/security/JwtAuthenticationFilter.java index cffc553e..db01f72f 100644 --- a/src/main/java/sopt/org/hmh/global/auth/security/JwtAuthenticationFilter.java +++ b/src/main/java/sopt/org/hmh/global/auth/security/JwtAuthenticationFilter.java @@ -11,14 +11,11 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import sopt.org.hmh.global.auth.jwt.JwtConstants; import sopt.org.hmh.global.auth.jwt.JwtProvider; import sopt.org.hmh.global.auth.jwt.JwtValidator; -import sopt.org.hmh.global.auth.jwt.exception.JwtError; -import sopt.org.hmh.global.auth.jwt.exception.JwtException; @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -34,15 +31,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } private String getAccessToken(HttpServletRequest request) { - String accessToken = request.getHeader(JwtConstants.AUTHORIZATION); - if (StringUtils.hasText(accessToken) && accessToken.startsWith(JwtConstants.BEARER)) { - return accessToken.substring(JwtConstants.BEARER.length()); - } - throw new JwtException(JwtError.INVALID_ACCESS_TOKEN); + return request.getHeader(JwtConstants.AUTHORIZATION); } - private void doAuthentication(HttpServletRequest request, Long userId) { - UserAuthentication authentication = createUserAuthentication(userId); + private void doAuthentication(HttpServletRequest request, String subjectId) { + UserAuthentication authentication = createUserAuthentication(subjectId); createAndSetWebAuthenticationDetails(request, authentication); SecurityContext securityContext = SecurityContextHolder.getContext(); securityContext.setAuthentication(authentication); diff --git a/src/main/java/sopt/org/hmh/global/auth/security/UserAuthentication.java b/src/main/java/sopt/org/hmh/global/auth/security/UserAuthentication.java index 7f5d5983..e3be2a1b 100644 --- a/src/main/java/sopt/org/hmh/global/auth/security/UserAuthentication.java +++ b/src/main/java/sopt/org/hmh/global/auth/security/UserAuthentication.java @@ -10,7 +10,7 @@ private UserAuthentication(Object principal, Object credentials, Collection resolvers) { resolvers.add(new UserIdArgumentResolver()); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(validateAdminInterceptor) + .addPathPatterns("/api/v1/admin/**") + .excludePathPatterns("/api/v1/admin/login"); + } } \ No newline at end of file