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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import com.yd.vibecode.domain.exam.application.dto.request.CreateExamRequest;
import com.yd.vibecode.domain.exam.application.dto.response.ExamResponse;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Random;

Expand All @@ -21,6 +22,7 @@
import com.yd.vibecode.domain.problem.infrastructure.repository.ProblemSetItemRepository;
import com.yd.vibecode.domain.problem.infrastructure.repository.ProblemSetRepository;
import com.yd.vibecode.global.exception.RestApiException;
import com.yd.vibecode.global.exception.code.status.ExamErrorStatus;
import com.yd.vibecode.global.exception.code.status.ProblemErrorStatus;
import com.yd.vibecode.global.util.CodeGenerator;

Expand All @@ -38,6 +40,10 @@ public class CreateExamUseCase {

@Transactional
public ExamResponse execute(Long adminId, CreateExamRequest request) {
if (!request.endsAt().isAfter(LocalDateTime.now())) {
throw new RestApiException(ExamErrorStatus.EXAM_ENDS_AT_NOT_IN_FUTURE);
}

// 1. 시험 생성
Exam exam = Exam.builder()
.title(request.title())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.yd.vibecode.domain.admin.application.usecase;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.yd.vibecode.domain.auth.domain.entity.Admin;
import com.yd.vibecode.domain.auth.domain.service.AdminAccountDeletionService;
import com.yd.vibecode.domain.auth.domain.service.AdminService;
import com.yd.vibecode.global.exception.RestApiException;
import com.yd.vibecode.global.exception.code.status.AuthErrorStatus;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class DeleteAdminByMasterUseCase {

private static final String AUDIT_ACTION = "DELETE_ADMIN_BY_MASTER";

private final AdminService adminService;
private final AdminAccountDeletionService adminAccountDeletionService;

@Transactional
public void execute(Long requesterId, String targetAdminNumber) {
Admin requester = adminService.findById(requesterId);
if (!requester.isMaster()) {
throw new RestApiException(AuthErrorStatus.MASTER_ONLY);
}

Admin target = adminService.findByAdminNumber(targetAdminNumber);
adminAccountDeletionService.softDelete(target, requesterId, AUDIT_ACTION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.yd.vibecode.domain.admin.application.usecase;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.yd.vibecode.domain.auth.application.usecase.AdminLogoutUseCase;
import com.yd.vibecode.domain.auth.domain.entity.Admin;
import com.yd.vibecode.domain.auth.domain.service.AdminAccountDeletionService;
import com.yd.vibecode.domain.auth.domain.service.AdminService;
import com.yd.vibecode.global.util.CookieUtils;

import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class DeleteOwnAdminAccountUseCase {

private static final String AUDIT_ACTION = "DELETE_OWN_ACCOUNT";

private final AdminService adminService;
private final AdminAccountDeletionService adminAccountDeletionService;
private final AdminLogoutUseCase adminLogoutUseCase;
private final CookieUtils cookieUtils;

@Transactional
public void execute(Long adminId, String accessToken, HttpServletResponse httpResponse) {
Admin admin = adminService.findById(adminId);
adminAccountDeletionService.softDelete(admin, adminId, AUDIT_ACTION);

adminLogoutUseCase.execute(accessToken);
cookieUtils.clearAccessTokenCookie(httpResponse);
cookieUtils.clearRefreshTokenCookie(httpResponse);
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package com.yd.vibecode.domain.admin.ui;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.yd.vibecode.domain.admin.application.dto.request.ChangeAdminPasswordRequest;
import com.yd.vibecode.domain.admin.application.usecase.ChangeAdminPasswordUseCase;
import com.yd.vibecode.domain.admin.application.usecase.DeleteOwnAdminAccountUseCase;
import com.yd.vibecode.global.swagger.AdminAccountApi;
import com.yd.vibecode.global.annotation.AccessToken;
import com.yd.vibecode.global.annotation.CurrentUser;
import com.yd.vibecode.global.common.BaseResponse;

import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

Expand All @@ -20,6 +25,7 @@
public class AdminAccountController implements AdminAccountApi {

private final ChangeAdminPasswordUseCase changeAdminPasswordUseCase;
private final DeleteOwnAdminAccountUseCase deleteOwnAdminAccountUseCase;

@PatchMapping("/password")
@Override
Expand All @@ -30,4 +36,18 @@ public BaseResponse<Void> changePassword(
changeAdminPasswordUseCase.execute(Long.parseLong(adminId), request);
return BaseResponse.onSuccess();
}

@DeleteMapping
@Operation(
summary = "본인 관리자 계정 삭제",
description = "현재 로그인한 관리자가 자신의 계정을 soft delete 합니다. 마스터 계정은 삭제할 수 없습니다."
)
public BaseResponse<Void> deleteOwnAccount(
@CurrentUser String adminId,
@AccessToken String accessToken,
HttpServletResponse httpResponse
) {
deleteOwnAdminAccountUseCase.execute(Long.parseLong(adminId), accessToken, httpResponse);
return BaseResponse.onSuccess();
}
}
Original file line number Diff line number Diff line change
@@ -1,71 +1,82 @@
package com.yd.vibecode.domain.admin.ui;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
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 com.yd.vibecode.domain.admin.application.dto.request.AdminNumberIssueRequest;
import com.yd.vibecode.domain.admin.application.dto.request.AdminNumberUpdateRequest;
import com.yd.vibecode.domain.admin.application.dto.response.AdminListResponse;
import com.yd.vibecode.domain.admin.application.dto.response.AdminNumberResponse;
import com.yd.vibecode.domain.admin.application.dto.response.ResetAdminPasswordByMasterResponse;
import com.yd.vibecode.domain.admin.application.usecase.GetAllAdminsUseCase;
import com.yd.vibecode.domain.admin.application.usecase.IssueAdminNumberUseCase;
import com.yd.vibecode.domain.admin.application.usecase.ResetAdminPasswordByMasterUseCase;
import com.yd.vibecode.domain.admin.application.usecase.UpdateAdminNumberUseCase;
import com.yd.vibecode.global.annotation.CurrentUser;
import com.yd.vibecode.global.common.BaseResponse;
import com.yd.vibecode.global.swagger.AdminNumberApi;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/admin/admin-numbers")
public class AdminNumberController implements AdminNumberApi {

private final IssueAdminNumberUseCase issueAdminNumberUseCase;
private final UpdateAdminNumberUseCase updateAdminNumberUseCase;
private final GetAllAdminsUseCase getAllAdminsUseCase;
private final ResetAdminPasswordByMasterUseCase resetAdminPasswordByMasterUseCase;

@GetMapping("/admins")
@Override
public BaseResponse<AdminListResponse> getAllAdmins(@CurrentUser String adminId) {
AdminListResponse response = getAllAdminsUseCase.execute(Long.parseLong(adminId));
return BaseResponse.onSuccess(response);
}

@PostMapping
@Override
public BaseResponse<AdminNumberResponse> issueAdminNumber(@CurrentUser String adminId,
@Valid @RequestBody AdminNumberIssueRequest request) {
AdminNumberResponse response = issueAdminNumberUseCase.execute(Long.parseLong(adminId), request);
return BaseResponse.onSuccess(response);
}

@PatchMapping("/{adminNumber}/password/reset")
@Override
public BaseResponse<ResetAdminPasswordByMasterResponse> resetAdminPasswordByMaster(
@CurrentUser String adminId,
@PathVariable String adminNumber) {
ResetAdminPasswordByMasterResponse response =
resetAdminPasswordByMasterUseCase.execute(Long.parseLong(adminId), adminNumber);
return BaseResponse.onSuccess(response);
}

@PatchMapping("/{adminNumber}")
@Override
public BaseResponse<AdminNumberResponse> updateAdminNumber(@CurrentUser String adminId,
@PathVariable String adminNumber,
@Valid @RequestBody AdminNumberUpdateRequest request) {
AdminNumberResponse response = updateAdminNumberUseCase.execute(Long.parseLong(adminId), adminNumber, request);
return BaseResponse.onSuccess(response);
}
}

package com.yd.vibecode.domain.admin.ui;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
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 com.yd.vibecode.domain.admin.application.dto.request.AdminNumberIssueRequest;
import com.yd.vibecode.domain.admin.application.dto.request.AdminNumberUpdateRequest;
import com.yd.vibecode.domain.admin.application.dto.response.AdminListResponse;
import com.yd.vibecode.domain.admin.application.dto.response.AdminNumberResponse;
import com.yd.vibecode.domain.admin.application.dto.response.ResetAdminPasswordByMasterResponse;
import com.yd.vibecode.domain.admin.application.usecase.DeleteAdminByMasterUseCase;
import com.yd.vibecode.domain.admin.application.usecase.GetAllAdminsUseCase;
import com.yd.vibecode.domain.admin.application.usecase.IssueAdminNumberUseCase;
import com.yd.vibecode.domain.admin.application.usecase.ResetAdminPasswordByMasterUseCase;
import com.yd.vibecode.domain.admin.application.usecase.UpdateAdminNumberUseCase;
import com.yd.vibecode.global.annotation.CurrentUser;
import com.yd.vibecode.global.common.BaseResponse;
import com.yd.vibecode.global.swagger.AdminNumberApi;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/admin/admin-numbers")
public class AdminNumberController implements AdminNumberApi {

private final IssueAdminNumberUseCase issueAdminNumberUseCase;
private final UpdateAdminNumberUseCase updateAdminNumberUseCase;
private final GetAllAdminsUseCase getAllAdminsUseCase;
private final ResetAdminPasswordByMasterUseCase resetAdminPasswordByMasterUseCase;
private final DeleteAdminByMasterUseCase deleteAdminByMasterUseCase;

@GetMapping("/admins")
@Override
public BaseResponse<AdminListResponse> getAllAdmins(@CurrentUser String adminId) {
AdminListResponse response = getAllAdminsUseCase.execute(Long.parseLong(adminId));
return BaseResponse.onSuccess(response);
}

@PostMapping
@Override
public BaseResponse<AdminNumberResponse> issueAdminNumber(@CurrentUser String adminId,
@Valid @RequestBody AdminNumberIssueRequest request) {
AdminNumberResponse response = issueAdminNumberUseCase.execute(Long.parseLong(adminId), request);
return BaseResponse.onSuccess(response);
}

@PatchMapping("/{adminNumber}/password/reset")
@Override
public BaseResponse<ResetAdminPasswordByMasterResponse> resetAdminPasswordByMaster(
@CurrentUser String adminId,
@PathVariable String adminNumber) {
ResetAdminPasswordByMasterResponse response =
resetAdminPasswordByMasterUseCase.execute(Long.parseLong(adminId), adminNumber);
return BaseResponse.onSuccess(response);
}

@PatchMapping("/{adminNumber}")
@Override
public BaseResponse<AdminNumberResponse> updateAdminNumber(@CurrentUser String adminId,
@PathVariable String adminNumber,
@Valid @RequestBody AdminNumberUpdateRequest request) {
AdminNumberResponse response = updateAdminNumberUseCase.execute(Long.parseLong(adminId), adminNumber, request);
return BaseResponse.onSuccess(response);
}

@DeleteMapping("/{adminNumber}")
@Override
public BaseResponse<Void> deleteAdminByMaster(@CurrentUser String adminId,
@PathVariable String adminNumber) {
deleteAdminByMasterUseCase.execute(Long.parseLong(adminId), adminNumber);
return BaseResponse.onSuccess();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public AdminLoginResponse execute(AdminLoginRequest request) {
admin = adminService.findByAdminNumber(request.identifier());
}

// 2. 계정 활성 상태 확인
if (!admin.getIsActive()) {
// 2. 삭제·비활성 계정 로그인 차단
if (admin.isDeleted() || !admin.getIsActive()) {
throw new RestApiException(AuthErrorStatus.ADMIN_ACCOUNT_INACTIVE);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.yd.vibecode.domain.auth.domain.service;

import org.springframework.stereotype.Component;

import com.yd.vibecode.domain.auth.domain.entity.Admin;
import com.yd.vibecode.domain.auth.domain.repository.AdminRepository;
import com.yd.vibecode.global.exception.RestApiException;
import com.yd.vibecode.global.exception.code.status.AuthErrorStatus;
import com.yd.vibecode.global.exception.code.status.GlobalErrorStatus;

import lombok.RequiredArgsConstructor;

/**
* JWT 인증 파이프라인용 관리자 access token 활성 상태 검증.
* PasswordEncoder 등 SecurityConfig Bean에 의존하지 않도록 분리한다.
*/
@Component
@RequiredArgsConstructor
public class AdminAccessAuthValidator {

private final AdminRepository adminRepository;

public void validateActiveForAuthentication(Long adminId) {
Admin admin = adminRepository.findById(adminId)
.orElseThrow(() -> new RestApiException(GlobalErrorStatus._NOT_FOUND));
if (admin.isDeleted() || !Boolean.TRUE.equals(admin.getIsActive())) {
throw new RestApiException(AuthErrorStatus.ADMIN_ACCOUNT_INACTIVE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.yd.vibecode.domain.auth.domain.service;

import java.util.Map;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.yd.vibecode.domain.admin.domain.service.AdminAuditLogService;
import com.yd.vibecode.domain.auth.domain.entity.Admin;
import com.yd.vibecode.domain.auth.domain.entity.AdminNumber;
import com.yd.vibecode.domain.auth.domain.repository.AdminNumberRepository;
import com.yd.vibecode.global.exception.RestApiException;
import com.yd.vibecode.global.exception.code.status.AuthErrorStatus;
import com.yd.vibecode.global.exception.code.status.GlobalErrorStatus;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AdminAccountDeletionService {

private final RefreshTokenService refreshTokenService;
private final AdminNumberRepository adminNumberRepository;
private final AdminAuditLogService adminAuditLogService;

@Transactional
public void softDelete(Admin admin, Long actorAdminId, String auditAction) {
if (admin.isDeleted()) {
throw new RestApiException(GlobalErrorStatus._NOT_FOUND);
}
if (admin.isMaster()) {
throw new RestApiException(AuthErrorStatus.MASTER_ACCOUNT_CANNOT_BE_DEACTIVATED);
}

admin.delete();
admin.updateActive(false);
refreshTokenService.deleteRefreshToken(admin.getId().toString());

adminNumberRepository.findByAdminNumber(admin.getAdminNumber())
.ifPresent(this::deactivateAdminNumber);

if (actorAdminId != null && auditAction != null) {
adminAuditLogService.log(actorAdminId, auditAction, Map.of(
"targetAdminId", admin.getId(),
"targetAdminNumber", admin.getAdminNumber()
));
}
}

private void deactivateAdminNumber(AdminNumber adminNumber) {
adminNumber.update(adminNumber.getLabel(), false, adminNumber.getExpiresAt());
}
}
Loading
Loading