diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 9e7ff50b..95e19532 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -261,20 +261,6 @@ include::{snippets}/auth-send-auth-code/http-response.adoc[] .Response Body include::{snippets}/auth-send-auth-code/response-fields.adoc[] -=== `GET`: 이메일 중복 확인 - -.HTTP Request -include::{snippets}/auth-check-duplicate-email/http-request.adoc[] - -.Path Parameters -include::{snippets}/auth-check-duplicate-email/query-parameters.adoc[] - -.HTTP Response -include::{snippets}/auth-check-duplicate-email/http-response.adoc[] - -.Response Body -include::{snippets}/auth-check-duplicate-email/response-fields.adoc[] - === `GET`: 학번 중복 확인 .HTTP Request @@ -289,20 +275,6 @@ include::{snippets}/auth-check-duplicate-student-id/http-response.adoc[] .Response Body include::{snippets}/auth-check-duplicate-student-id/response-fields.adoc[] -=== `GET`: 전화번호 중복 확인 - -.HTTP Request -include::{snippets}/auth-check-duplicate-phone-number/http-request.adoc[] - -.Path Parameters -include::{snippets}/auth-check-duplicate-phone-number/query-parameters.adoc[] - -.HTTP Response -include::{snippets}/auth-check-duplicate-phone-number/http-response.adoc[] - -.Response Body -include::{snippets}/auth-check-duplicate-phone-number/response-fields.adoc[] - === `POST`: 로그인 .HTTP Request @@ -328,3 +300,38 @@ include::{snippets}/auth-reset-password/request-fields.adoc[] .HTTP Response include::{snippets}/auth-reset-password/http-response.adoc[] +== 관리자 인증 + +=== `POST`: 관리자 단일 등록 + +.HTTP Request +include::{snippets}/admin-auth-register/http-request.adoc[] + +.Request Body +include::{snippets}/admin-auth-register/request-body.adoc[] + +.HTTP Response +include::{snippets}/admin-auth-register/http-response.adoc[] + +=== `POST`: 파일을 이용한 관리자 다중 등록 + +.HTTP Request +include::{snippets}/admin-auth-register-by-file/http-request.adoc[] + +.Request Body +include::{snippets}/admin-auth-register-by-file/request-parts.adoc[] + +.HTTP Response +include::{snippets}/admin-auth-register-by-file/http-response.adoc[] + +=== `DELETE`: 관리자 삭제 + +.HTTP Request +include::{snippets}/admin-auth-delete/http-request.adoc[] + +.Request Body +include::{snippets}/admin-auth-delete/request-body.adoc[] + +.HTTP Response +include::{snippets}/admin-auth-delete/http-response.adoc[] + diff --git a/backend/src/main/java/sw_css/admin/auth/api/AdminAuthController.java b/backend/src/main/java/sw_css/admin/auth/api/AdminAuthController.java new file mode 100644 index 00000000..b6e30916 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/auth/api/AdminAuthController.java @@ -0,0 +1,54 @@ +package sw_css.admin.auth.api; + +import jakarta.validation.Valid; +import java.net.URI; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import sw_css.admin.auth.application.AdminAuthCommandService; +import sw_css.admin.auth.application.dto.request.DeleteFacultyRequest; +import sw_css.admin.auth.application.dto.request.RegisterFacultyRequest; +import sw_css.member.domain.FacultyMember; +import sw_css.utils.annotation.Admin; +import sw_css.utils.annotation.SuperAdmin; + +@Validated +@RequestMapping("/admin/auth") +@RestController +@RequiredArgsConstructor +public class AdminAuthController { + + private final AdminAuthCommandService adminAuthCommandService; + + @PostMapping + public ResponseEntity registerFaculty( + @Admin FacultyMember facultyMember, + @RequestBody @Valid RegisterFacultyRequest request) { + Long memberId = adminAuthCommandService.registerFaculty(request); + return ResponseEntity.created(URI.create("/members/" + memberId)).build(); + } + + @PostMapping("/files") + public ResponseEntity registerFaculties( + @Admin FacultyMember facultyMember, + @RequestPart(value = "file") final MultipartFile file) { + adminAuthCommandService.registerFaculties(file); + return ResponseEntity.created(URI.create("/admin/faculties")).build(); + } + + @DeleteMapping() + public ResponseEntity deleteFaculty( + @SuperAdmin FacultyMember facultyMember, + @RequestBody @Valid DeleteFacultyRequest request) { + adminAuthCommandService.deleteFaculty(request.member_id()); + return ResponseEntity.noContent().build(); + } + +} diff --git a/backend/src/main/java/sw_css/admin/auth/application/AdminAuthCommandService.java b/backend/src/main/java/sw_css/admin/auth/application/AdminAuthCommandService.java new file mode 100644 index 00000000..97a40999 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/auth/application/AdminAuthCommandService.java @@ -0,0 +1,156 @@ +package sw_css.admin.auth.application; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.commons.io.FilenameUtils; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import sw_css.admin.auth.application.dto.request.RegisterFacultyRequest; +import sw_css.admin.auth.exception.AdminAuthException; +import sw_css.admin.auth.exception.AdminAuthExceptionType; +import sw_css.auth.application.AuthCheckDuplicateService; +import sw_css.member.domain.FacultyMember; +import sw_css.member.domain.Member; +import sw_css.member.domain.embedded.EmailAddress; +import sw_css.member.domain.embedded.Password; +import sw_css.member.domain.embedded.RealName; +import sw_css.member.domain.repository.FacultyMemberRepository; +import sw_css.member.domain.repository.MemberRepository; + +@Service +@RequiredArgsConstructor +public class AdminAuthCommandService { + + private final MemberRepository memberRepository; + private final FacultyMemberRepository facultyMemberRepository; + private final AuthCheckDuplicateService authCheckDuplicateService; + + @Value("${password.admin}") + private String password; + + @Transactional + public Long registerFaculty(RegisterFacultyRequest request) { + validateDuplicateEmail(request.email()); + + final String encodedPassword = Password.encode(password); + + final long memberId = memberRepository.save(request.toMember(encodedPassword)).getId(); + facultyMemberRepository.save(request.toFacultyMember(memberId, encodedPassword)); + + return memberId; + } + + public void registerFaculties(MultipartFile file) { + final String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + if (extension == null || !(extension.equals("xlsx") || extension.equals("xls"))) { + throw new AdminAuthException(AdminAuthExceptionType.NO_MATCH_EXTENSION); + } + + final String encodedPassword = Password.encode(password); + final List failedData = new ArrayList<>(); + + final Workbook workbook = generateWorkbook(file, extension); + + final Sheet worksheet = workbook.getSheetAt(0); + for (int i = 1; i < worksheet.getPhysicalNumberOfRows(); i++) { + final Row row = worksheet.getRow(i); + + final String email = row.getCell(0).getStringCellValue(); + final String name = row.getCell(1).getStringCellValue(); + + if (isInvalidInput(email, name)) { + failedData.add(i + 1); + continue; + } + + saveFaculty(email, name, encodedPassword); + } + + checkFailedData(failedData); + } + + @Transactional + public void deleteFaculty(Long memberId) { + FacultyMember facultyMember = facultyMemberRepository.findById(memberId) + .orElseThrow(() -> new AdminAuthException(AdminAuthExceptionType.MEMBER_NOT_FOUND)); + + Member member = facultyMember.getMember(); + + checkIsMemberDeleted(member); + + member.setDeleted(true); + memberRepository.save(member); + } + + private Workbook generateWorkbook(final MultipartFile file, String extension) { + try { + if (extension.equals("xlsx")) { + return new XSSFWorkbook(file.getInputStream()); + } + return new HSSFWorkbook(file.getInputStream()); + } catch (final IOException exception) { + throw new AdminAuthException(AdminAuthExceptionType.CANNOT_OPEN_FILE); + } + } + + private void saveFaculty(final String email, final String name, final String password) { + Member member = new Member(email, name, password, "01000000000", false); + + final Member savedMember = memberRepository.save(member); + + FacultyMember facultyMember = new FacultyMember(null, savedMember); + facultyMemberRepository.save(facultyMember); + } + + private void validateDuplicateEmail(String email) { + if (authCheckDuplicateService.isDuplicateEmail(email)) { + throw new AdminAuthException(AdminAuthExceptionType.MEMBER_EMAIL_DUPLICATE); + } + } + + private boolean isDuplicateEmail(String email) { + return authCheckDuplicateService.isDuplicateEmail(email); + } + + private boolean isInvalidInput(final String email, final String name) { + if (Pattern.matches(EmailAddress.EMAIL_ADDRESS_REGEX, email) && + Pattern.matches(RealName.NAME_REGEX, name) && !isDuplicateEmail(email)) { + return false; + } + return true; + } + + private void checkFailedData(final List failedData) { + if (failedData.isEmpty()) { + return; + } + + String ids = failedData.stream() + .map(Object::toString) + .collect(Collectors.joining(", ")); + AdminAuthExceptionType exceptionType = AdminAuthExceptionType.FAILED_REGISTER_FACULTY; + exceptionType.setErrorMessage(ids + "번째 줄의 관리자를 등록하는데 실패했습니다."); + + throw new AdminAuthException(exceptionType); + } + + private void checkIsMemberDeleted(final Member member) { + if (!member.isDeleted()) { + return; + } + throw new AdminAuthException(AdminAuthExceptionType.MEMBER_NOT_FOUND); + + } + +} diff --git a/backend/src/main/java/sw_css/admin/auth/application/dto/request/DeleteFacultyRequest.java b/backend/src/main/java/sw_css/admin/auth/application/dto/request/DeleteFacultyRequest.java new file mode 100644 index 00000000..8927bace --- /dev/null +++ b/backend/src/main/java/sw_css/admin/auth/application/dto/request/DeleteFacultyRequest.java @@ -0,0 +1,6 @@ +package sw_css.admin.auth.application.dto.request; + +public record DeleteFacultyRequest( + Long member_id +) { +} diff --git a/backend/src/main/java/sw_css/admin/auth/application/dto/request/RegisterFacultyRequest.java b/backend/src/main/java/sw_css/admin/auth/application/dto/request/RegisterFacultyRequest.java new file mode 100644 index 00000000..21125da4 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/auth/application/dto/request/RegisterFacultyRequest.java @@ -0,0 +1,29 @@ +package sw_css.admin.auth.application.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Pattern; +import sw_css.member.domain.FacultyMember; +import sw_css.member.domain.Member; +import sw_css.member.domain.embedded.EmailAddress; +import sw_css.member.domain.embedded.RealName; + +public record RegisterFacultyRequest( + @Email(message = "이메일 형식을 확인해주세요.") + @Pattern(regexp = EmailAddress.EMAIL_ADDRESS_REGEX, message = EmailAddress.EMAIL_ADDRESS_INVALID) + String email, + @Pattern(regexp = RealName.NAME_REGEX, message = RealName.NAME_INVALID) + String name) { + + public Member toMember(String password) { + return new Member(email, name, password, "01000000000", false); + } + + public Member toMember(Long memberId, String password) { + return new Member(memberId, email, name, password, "01000000000", false); + } + + public FacultyMember toFacultyMember(Long memberId, String password) { + final Member member = toMember(memberId, password); + return new FacultyMember(null, member); + } +} diff --git a/backend/src/main/java/sw_css/admin/auth/exception/AdminAuthException.java b/backend/src/main/java/sw_css/admin/auth/exception/AdminAuthException.java new file mode 100644 index 00000000..781ae3cc --- /dev/null +++ b/backend/src/main/java/sw_css/admin/auth/exception/AdminAuthException.java @@ -0,0 +1,19 @@ +package sw_css.admin.auth.exception; + + +import sw_css.base.BaseException; +import sw_css.base.BaseExceptionType; + +public class AdminAuthException extends BaseException { + private final AdminAuthExceptionType adminAuthExceptionType; + + public AdminAuthException(final AdminAuthExceptionType exceptionType) { + super(exceptionType.errorMessage()); + this.adminAuthExceptionType = exceptionType; + } + + @Override + public BaseExceptionType exceptionType() { + return adminAuthExceptionType; + } +} diff --git a/backend/src/main/java/sw_css/admin/auth/exception/AdminAuthExceptionType.java b/backend/src/main/java/sw_css/admin/auth/exception/AdminAuthExceptionType.java new file mode 100644 index 00000000..2ae597c1 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/auth/exception/AdminAuthExceptionType.java @@ -0,0 +1,34 @@ +package sw_css.admin.auth.exception; + +import lombok.Setter; +import org.springframework.http.HttpStatus; +import sw_css.base.BaseExceptionType; + +public enum AdminAuthExceptionType implements BaseExceptionType { + NO_MATCH_EXTENSION(HttpStatus.BAD_REQUEST, "파일 확장자가 올바르지 않습니다."), + CANNOT_OPEN_FILE(HttpStatus.BAD_REQUEST, "파일을 열 수 없습니다."), + FAILED_REGISTER_FACULTY(HttpStatus.BAD_REQUEST, ""), + MEMBER_EMAIL_DUPLICATE(HttpStatus.CONFLICT, "이메일이 중복됩니다."), + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원을 찾을 수 없습니다."), + ; + + private final HttpStatus httpStatus; + @Setter + private String errorMessage; + + AdminAuthExceptionType(final HttpStatus httpStatus, final String errorMessage) { + this.httpStatus = httpStatus; + this.errorMessage = errorMessage; + } + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public String errorMessage() { + return errorMessage; + } + +} diff --git a/backend/src/main/java/sw_css/auth/api/SignUpController.java b/backend/src/main/java/sw_css/auth/api/SignUpController.java index 5a1ebbd6..a8994117 100644 --- a/backend/src/main/java/sw_css/auth/api/SignUpController.java +++ b/backend/src/main/java/sw_css/auth/api/SignUpController.java @@ -40,21 +40,9 @@ public ResponseEntity sendAuthCode(@RequestBody @Valid Sen return ResponseEntity.ok(authEmailService.emailAuth(request.email())); } - @GetMapping("/exists/email") - public ResponseEntity checkDuplicateEmail( - @RequestParam(value = "email") @NotBlank final String email) { - return ResponseEntity.ok(authSignUpService.isDuplicateEmail(email)); - } - @GetMapping("/exists/student-id") public ResponseEntity checkDuplicateStudentId( @RequestParam(value = "student_id") @NotBlank final String studentId) { return ResponseEntity.ok(authSignUpService.isDuplicateStudentId(studentId)); } - - @GetMapping("/exists/phone-number") - public ResponseEntity checkDuplicatePhoneNumber( - @RequestParam(value = "phone_number") @NotBlank final String phoneNumber) { - return ResponseEntity.ok(authSignUpService.isDuplicatePhoneNumber(phoneNumber)); - } } diff --git a/backend/src/main/java/sw_css/auth/application/AuthCheckDuplicateService.java b/backend/src/main/java/sw_css/auth/application/AuthCheckDuplicateService.java index 1457af5a..e126cb93 100644 --- a/backend/src/main/java/sw_css/auth/application/AuthCheckDuplicateService.java +++ b/backend/src/main/java/sw_css/auth/application/AuthCheckDuplicateService.java @@ -21,8 +21,4 @@ public boolean isDuplicateStudentID(String studentIdStr) { Long studentId = Long.parseLong(studentIdStr); return studentMemberRepository.existsById(studentId); } - - public boolean isDuplicatePhoneNumber(String phoneNumber) { - return memberRepository.existsByPhoneNumber(phoneNumber); - } } diff --git a/backend/src/main/java/sw_css/auth/application/AuthEmailService.java b/backend/src/main/java/sw_css/auth/application/AuthEmailService.java index 336daf02..033cb25d 100644 --- a/backend/src/main/java/sw_css/auth/application/AuthEmailService.java +++ b/backend/src/main/java/sw_css/auth/application/AuthEmailService.java @@ -1,5 +1,6 @@ package sw_css.auth.application; +import jakarta.transaction.Transactional; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -10,10 +11,12 @@ import sw_css.auth.domain.repository.EmailAuthRedisRepository; import sw_css.auth.exception.AuthException; import sw_css.auth.exception.AuthExceptionType; +import sw_css.member.domain.repository.MemberRepository; import sw_css.utils.MailUtil; @Service @RequiredArgsConstructor +@Transactional(rollbackOn = Exception.class) public class AuthEmailService { public static final int EMAIL_EXPIRED_SECONDS = 600; // 10분 public static final int AUTH_CODE_LENGTH = 10; @@ -23,6 +26,7 @@ public class AuthEmailService { private final MailUtil mailUtil; private final EmailAuthRedisRepository emailAuthRedisRepository; private final AuthCheckDuplicateService authCheckDuplicateService; + private final MemberRepository memberRepository; public SendAuthCodeResponse emailAuth(String email) { checkIsDuplicateEmail(email); diff --git a/backend/src/main/java/sw_css/auth/application/AuthSignInService.java b/backend/src/main/java/sw_css/auth/application/AuthSignInService.java index 7297aa56..421202e7 100644 --- a/backend/src/main/java/sw_css/auth/application/AuthSignInService.java +++ b/backend/src/main/java/sw_css/auth/application/AuthSignInService.java @@ -35,29 +35,26 @@ public class AuthSignInService { public SignInResponse signIn(String email, String rawPassword) { Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new AuthException(AuthExceptionType.MEMBER_EMAIL_NOT_FOUND)); + .orElseThrow(() -> new AuthException(AuthExceptionType.MEMBER_NOT_REGISTER)); - if (member.isWrongPassword(rawPassword)) { - throw new AuthException(AuthExceptionType.MEMBER_WRONG_ID_OR_PASSWORD); - } + checkIsMemberDeleted(member); + checkIsValidPassword(member, rawPassword); Object memberDetail = loadMemberDetail(member); if (memberDetail instanceof StudentMember studentMember) { String role = Role.ROLE_MEMBER.toString(); - String accessToken = jwtTokenProvider.createToken(member.getId(), role); return SignInResponse.of(member, studentMember.getId(), role, false, accessToken); } else if (memberDetail instanceof FacultyMember facultyMember) { String role = Role.ROLE_ADMIN.toString(); - String accessToken = jwtTokenProvider.createToken(member.getId(), role); return SignInResponse.of(member, facultyMember.getId(), role, true, accessToken); } - throw new AuthException(AuthExceptionType.MEMBER_EMAIL_NOT_FOUND); + throw new AuthException(AuthExceptionType.MEMBER_NOT_REGISTER); } public void resetPassword(String email, String name) { @@ -116,4 +113,18 @@ private static Boolean isAvailableSpecialCharacter(int i) { String availableSpecialCharacters = "!@#%^&*"; return availableSpecialCharacters.indexOf(c) != -1; } + + private void checkIsMemberDeleted(Member member) { + if (!member.isDeleted()) { + return; + } + throw new AuthException(AuthExceptionType.MEMBER_NOT_REGISTER); + } + + private void checkIsValidPassword(Member member, String password) { + if (!member.isWrongPassword(password)) { + return; + } + throw new AuthException(AuthExceptionType.MEMBER_WRONG_ID_OR_PASSWORD); + } } diff --git a/backend/src/main/java/sw_css/auth/application/AuthSignUpService.java b/backend/src/main/java/sw_css/auth/application/AuthSignUpService.java index 45a4e57f..fffec24d 100644 --- a/backend/src/main/java/sw_css/auth/application/AuthSignUpService.java +++ b/backend/src/main/java/sw_css/auth/application/AuthSignUpService.java @@ -30,8 +30,7 @@ public long signUp(SignUpRequest request) { checkIsDuplicateEmail(request.email()); checkIsDuplicateStudentId(request.student_id()); - String actualAuthCode = loadActualAuthCode(request.email()); - checkAuthCodeMatch(request.auth_code(), actualAuthCode); + checkIsValidAuthCode(request.email(), request.auth_code()); Major major = majorRepository.findById(request.major_id()) .orElseThrow(() -> new AuthException(AuthExceptionType.MAJOR_NOT_EXIST)); @@ -49,18 +48,10 @@ public long signUp(SignUpRequest request) { return memberId; } - public CheckDuplicateResponse isDuplicateEmail(String email) { - return CheckDuplicateResponse.from(authCheckDuplicateService.isDuplicateEmail(email)); - } - public CheckDuplicateResponse isDuplicateStudentId(String studentId) { return CheckDuplicateResponse.from(authCheckDuplicateService.isDuplicateStudentID(studentId)); } - public CheckDuplicateResponse isDuplicatePhoneNumber(String phoneNumber) { - return CheckDuplicateResponse.from(authCheckDuplicateService.isDuplicatePhoneNumber(phoneNumber)); - } - private void checkIsDuplicateEmail(String email) { if (authCheckDuplicateService.isDuplicateEmail(email)) { throw new AuthException(AuthExceptionType.MEMBER_EMAIL_DUPLICATE); @@ -73,12 +64,6 @@ private void checkIsDuplicateStudentId(String studentId) { } } - private void checkIsDuplicatePhoneNumber(String phoneNumber) { - if (authCheckDuplicateService.isDuplicatePhoneNumber(phoneNumber)) { - throw new AuthException(AuthExceptionType.MEMBER_PHONE_NUMBER_DUPLICATE); - } - } - private void checkAuthCodeMatch(String requestAuthCode, String actualAuthCode) { if (!actualAuthCode.equals(requestAuthCode)) { throw new AuthException(AuthExceptionType.AUTH_CODE_MISMATCH); @@ -90,4 +75,9 @@ private String loadActualAuthCode(@Email String email) { .orElseThrow(() -> new AuthException(AuthExceptionType.AUTH_CODE_EXPIRED)) .getAuthCode(); } + + private void checkIsValidAuthCode(String email, String authCode) { + String actualAuthCode = loadActualAuthCode(email); + checkAuthCodeMatch(authCode, actualAuthCode); + } } diff --git a/backend/src/main/java/sw_css/auth/exception/AuthExceptionType.java b/backend/src/main/java/sw_css/auth/exception/AuthExceptionType.java index 91813c38..84175248 100644 --- a/backend/src/main/java/sw_css/auth/exception/AuthExceptionType.java +++ b/backend/src/main/java/sw_css/auth/exception/AuthExceptionType.java @@ -13,6 +13,7 @@ public enum AuthExceptionType implements BaseExceptionType { MEMBER_EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원의 이메일을 찾을 수 없습니다."), MEMBER_WRONG_ID_OR_PASSWORD(HttpStatus.BAD_REQUEST, "아이디 혹은 비밀번호가 잘못되었습니다."), MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원의 정보를 찾을 수 없습니다."), + MEMBER_NOT_REGISTER(HttpStatus.BAD_REQUEST, "등록되지 않은 회원입니다."), MEMBER_NOT_MATCH_EMAIL_AND_NAME(HttpStatus.BAD_REQUEST, "회원의 이메일과 이름이 일치하지 않습니다."); private final HttpStatus httpStatus; diff --git a/backend/src/main/java/sw_css/config/WebConfig.java b/backend/src/main/java/sw_css/config/WebConfig.java index af7488b3..24d10212 100644 --- a/backend/src/main/java/sw_css/config/WebConfig.java +++ b/backend/src/main/java/sw_css/config/WebConfig.java @@ -5,16 +5,22 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import sw_css.utils.JwtToken.AdminArgumentResolver; import sw_css.utils.JwtToken.JwtAuthorizationArgumentResolver; +import sw_css.utils.JwtToken.SuperAdminArgumentResolver; @Configuration @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { private final JwtAuthorizationArgumentResolver jwtAuthorizationArgumentResolver; + private final AdminArgumentResolver adminArgumentResolver; + private final SuperAdminArgumentResolver superAdminArgumentResolver; @Override public void addArgumentResolvers(List resolvers) { resolvers.add(jwtAuthorizationArgumentResolver); + resolvers.add(adminArgumentResolver); + resolvers.add(superAdminArgumentResolver); } } diff --git a/backend/src/main/java/sw_css/member/domain/FacultyMember.java b/backend/src/main/java/sw_css/member/domain/FacultyMember.java index cbadc4d3..79dbbc62 100644 --- a/backend/src/main/java/sw_css/member/domain/FacultyMember.java +++ b/backend/src/main/java/sw_css/member/domain/FacultyMember.java @@ -2,6 +2,8 @@ import jakarta.persistence.Entity; import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; @@ -17,6 +19,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class FacultyMember extends BaseEntity { @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) diff --git a/backend/src/main/java/sw_css/member/domain/Member.java b/backend/src/main/java/sw_css/member/domain/Member.java index c60dcd39..28c6abc5 100644 --- a/backend/src/main/java/sw_css/member/domain/Member.java +++ b/backend/src/main/java/sw_css/member/domain/Member.java @@ -10,6 +10,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.hibernate.annotations.SQLRestriction; import sw_css.base.BaseEntity; import sw_css.member.domain.embedded.Password; @@ -17,6 +18,7 @@ @Getter @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLRestriction("is_deleted = false") public class Member extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -35,6 +37,7 @@ public class Member extends BaseEntity { @Column(nullable = false) private String phoneNumber; + @Setter(AccessLevel.PUBLIC) @Column(nullable = false) private boolean isDeleted; diff --git a/backend/src/main/java/sw_css/member/domain/repository/MemberRepository.java b/backend/src/main/java/sw_css/member/domain/repository/MemberRepository.java index 702e7dd2..cd603ac2 100644 --- a/backend/src/main/java/sw_css/member/domain/repository/MemberRepository.java +++ b/backend/src/main/java/sw_css/member/domain/repository/MemberRepository.java @@ -7,7 +7,5 @@ public interface MemberRepository extends JpaRepository { boolean existsByEmail(String email); - boolean existsByPhoneNumber(String phoneNumber); - Optional findByEmail(String email); } diff --git a/backend/src/main/java/sw_css/utils/JwtToken/AdminArgumentResolver.java b/backend/src/main/java/sw_css/utils/JwtToken/AdminArgumentResolver.java new file mode 100644 index 00000000..d8b8d909 --- /dev/null +++ b/backend/src/main/java/sw_css/utils/JwtToken/AdminArgumentResolver.java @@ -0,0 +1,55 @@ +package sw_css.utils.JwtToken; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import sw_css.member.domain.Member; +import sw_css.member.domain.repository.FacultyMemberRepository; +import sw_css.member.domain.repository.MemberRepository; +import sw_css.member.exception.MemberException; +import sw_css.member.exception.MemberExceptionType; +import sw_css.utils.JwtToken.exception.JwtTokenException; +import sw_css.utils.JwtToken.exception.JwtTokenExceptionType; +import sw_css.utils.annotation.Admin; + +@Component +@RequiredArgsConstructor +public class AdminArgumentResolver implements HandlerMethodArgumentResolver { + private final MemberRepository memberRepository; + private final FacultyMemberRepository facultyMemberRepository; + private final JwtTokenProvider jwtTokenProvider; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Admin.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + + if (httpServletRequest == null) { + throw new JwtTokenException(JwtTokenExceptionType.JWT_TOKEN_INACCESSIBLE); + } + + String token = httpServletRequest.getHeader("Authorization"); + if (token == null || token.trim().isEmpty()) { + throw new JwtTokenException(JwtTokenExceptionType.JWT_TOKEN_EMPTY); + } + + jwtTokenProvider.validateToken(token); + + long userId = jwtTokenProvider.getUserId(token); + Member member = memberRepository.findById(userId) + .orElseThrow(() -> new MemberException(MemberExceptionType.MEMBER_NOT_FOUND)); + + return facultyMemberRepository.findByMemberId(member.getId()) + .orElseThrow(() -> new JwtTokenException(JwtTokenExceptionType.JWT_TOKEN_INACCESSIBLE)); + } +} diff --git a/backend/src/main/java/sw_css/utils/JwtToken/SuperAdminArgumentResolver.java b/backend/src/main/java/sw_css/utils/JwtToken/SuperAdminArgumentResolver.java new file mode 100644 index 00000000..31dc9905 --- /dev/null +++ b/backend/src/main/java/sw_css/utils/JwtToken/SuperAdminArgumentResolver.java @@ -0,0 +1,60 @@ +package sw_css.utils.JwtToken; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import sw_css.member.domain.Member; +import sw_css.member.domain.repository.FacultyMemberRepository; +import sw_css.member.domain.repository.MemberRepository; +import sw_css.member.exception.MemberException; +import sw_css.member.exception.MemberExceptionType; +import sw_css.utils.JwtToken.exception.JwtTokenException; +import sw_css.utils.JwtToken.exception.JwtTokenExceptionType; +import sw_css.utils.annotation.Admin; + +@Component +@RequiredArgsConstructor +public class SuperAdminArgumentResolver implements HandlerMethodArgumentResolver { + private final MemberRepository memberRepository; + private final FacultyMemberRepository facultyMemberRepository; + private final JwtTokenProvider jwtTokenProvider; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Admin.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + + if (httpServletRequest == null) { + throw new JwtTokenException(JwtTokenExceptionType.JWT_TOKEN_INACCESSIBLE); + } + + String token = httpServletRequest.getHeader("Authorization"); + if (token == null || token.trim().isEmpty()) { + throw new JwtTokenException(JwtTokenExceptionType.JWT_TOKEN_EMPTY); + } + + jwtTokenProvider.validateToken(token); + + long userId = jwtTokenProvider.getUserId(token); + Member member = memberRepository.findById(userId) + .orElseThrow(() -> new MemberException(MemberExceptionType.MEMBER_NOT_FOUND)); + + if (member.getId() != 1) { + throw new JwtTokenException(JwtTokenExceptionType.JWT_TOKEN_EMPTY); + } + + return facultyMemberRepository.findByMemberId(member.getId()) + .orElseThrow(() -> new JwtTokenException(JwtTokenExceptionType.JWT_TOKEN_INACCESSIBLE)); + } +} + diff --git a/backend/src/main/java/sw_css/utils/annotation/Admin.java b/backend/src/main/java/sw_css/utils/annotation/Admin.java new file mode 100644 index 00000000..0d149928 --- /dev/null +++ b/backend/src/main/java/sw_css/utils/annotation/Admin.java @@ -0,0 +1,12 @@ +package sw_css.utils.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Admin { + boolean required() default true; +} diff --git a/backend/src/main/java/sw_css/utils/annotation/JwtAuthorization.java b/backend/src/main/java/sw_css/utils/annotation/JwtAuthorization.java index 7efe054d..6a886fb5 100644 --- a/backend/src/main/java/sw_css/utils/annotation/JwtAuthorization.java +++ b/backend/src/main/java/sw_css/utils/annotation/JwtAuthorization.java @@ -5,7 +5,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; - @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface JwtAuthorization { diff --git a/backend/src/main/java/sw_css/utils/annotation/SuperAdmin.java b/backend/src/main/java/sw_css/utils/annotation/SuperAdmin.java new file mode 100644 index 00000000..c13cfb3a --- /dev/null +++ b/backend/src/main/java/sw_css/utils/annotation/SuperAdmin.java @@ -0,0 +1,12 @@ +package sw_css.utils.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface SuperAdmin { + boolean required() default true; +} diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index f8b6ccd0..f34a016e 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit f8b6ccd0232837bbda578eb6f953ec27ef55d8fb +Subproject commit f34a016ecd1faba0a310178b3d98780f908c0d28 diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql index 108f49c4..91f3297b 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -32,7 +32,7 @@ create table student_member create table faculty_member ( - id bigint primary key, + id bigint auto_increment primary key, member_id bigint not null, created_at datetime(6) not null default current_timestamp(6) ); diff --git a/backend/src/main/resources/test-data.sql b/backend/src/main/resources/test-data.sql index a2dcc63d..82e6aca9 100644 --- a/backend/src/main/resources/test-data.sql +++ b/backend/src/main/resources/test-data.sql @@ -1,24 +1,17 @@ ## member(student) insert into member (email, name, password, phone_number, is_deleted) -values ('songsy405@pusan.ac.kr', '송세연', '$2a$10$YyiOL/E5WjKrZPkB6eQSK.PwZtAO.z3JimFbq/Ky3u3rFf3XTGrWK', '010-0000-0000', +values ('admin@pusan.ac.kr', '관리자', '$2a$10$YyiOL/E5WjKrZPkB6eQSK.PwZtAO.z3JimFbq/Ky3u3rFf3XTGrWK', '01000000000', false); -insert into student_member (id, member_id, major_id, minor_id, double_major_id, career, career_detail) -values (202055558, 1, 1, null, null, 'EMPLOYMENT_COMPANY', 'IT 기업 개발자'); +insert into faculty_member (member_id) +values (1); insert into member (email, name, password, phone_number, is_deleted) -values ( 'ddang@pusan.ac.kr', '이다은', '$2a$10$YyiOL/E5WjKrZPkB6eQSK.PwZtAO.z3JimFbq/Ky3u3rFf3XTGrWK', '010-0000-0000' +values ( 'ddang@pusan.ac.kr', '이다은', '$2a$10$YyiOL/E5WjKrZPkB6eQSK.PwZtAO.z3JimFbq/Ky3u3rFf3XTGrWK', '01000000000' , false); insert into student_member (id, member_id, major_id, minor_id, double_major_id, career, career_detail) -values (202055555, 2, 1, null, null, 'GRADUATE_SCHOOL', 'IT 기업 개발자'); - -insert into member (email, name, password, phone_number, is_deleted) -values ( 'hayun@pusan.ac.kr', '김하윤', '$2a$10$YyiOL/E5WjKrZPkB6eQSK.PwZtAO.z3JimFbq/Ky3u3rFf3XTGrWK', '010-0000-0000' - , false); - -insert into faculty_member (id, member_id) -values (1, 3); +values (202055555, 3, 1, null, null, 'GRADUATE_SCHOOL', 'IT 기업 개발자'); ## milestone histories INSERT INTO sw_css.milestone_history (id, milestone_id, student_id, description, file_url, status, reject_reason, count, diff --git a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java index a508ce9a..3b35c8e2 100644 --- a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java @@ -14,6 +14,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.filter.CharacterEncodingFilter; +import sw_css.admin.auth.application.AdminAuthCommandService; import sw_css.admin.member.application.MemberAdminQueryService; import sw_css.admin.milestone.application.MilestoneHistoryAdminCommandService; import sw_css.admin.milestone.application.MilestoneHistoryAdminQueryService; @@ -25,10 +26,13 @@ import sw_css.helper.ApiTestHelper; import sw_css.major.application.MajorQueryService; import sw_css.member.application.MemberQueryService; +import sw_css.member.domain.repository.MemberRepository; import sw_css.milestone.application.MilestoneHistoryCommandService; import sw_css.milestone.application.MilestoneHistoryQueryService; import sw_css.milestone.application.MilestoneQueryService; +import sw_css.utils.JwtToken.AdminArgumentResolver; import sw_css.utils.JwtToken.JwtAuthorizationArgumentResolver; +import sw_css.utils.JwtToken.SuperAdminArgumentResolver; @Import(RestDocsConfiguration.class) @ExtendWith(RestDocumentationExtension.class) @@ -73,9 +77,21 @@ public abstract class RestDocsTest extends ApiTestHelper { @MockBean protected AuthSignInService authSignInService; + @MockBean + protected AdminAuthCommandService adminAuthCommandService; + + @MockBean + protected MemberRepository memberRepository; + @MockBean protected JwtAuthorizationArgumentResolver jwtAuthorizationArgumentResolver; + @MockBean + protected AdminArgumentResolver adminArgumentResolver; + + @MockBean + protected SuperAdminArgumentResolver superAdminArgumentResolver; + @Autowired protected RestDocumentationResultHandler restDocs; diff --git a/backend/src/test/java/sw_css/restdocs/docs/SignUpApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/SignUpApiDocsTest.java index e255db4f..cc44dbb2 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/SignUpApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/SignUpApiDocsTest.java @@ -99,30 +99,6 @@ public void sendAuthCodeMail() throws Exception { .andDo(document("auth-send-auth-code", requestFieldsSnippet, responseFieldsSnippet)); } - @Test - @DisplayName("[성공] 중복하는 이메일인지 확인할 수 있다.") - public void checkDuplicateEmail() throws Exception { - // given - final QueryParametersSnippet queryParameters = queryParameters( - parameterWithName("email").description("부산대학교 이메일") - ); - final ResponseFieldsSnippet responseBodySnippet = responseFields( - fieldWithPath("is_duplicate").type(JsonFieldType.BOOLEAN).description("중복 여부")); - - final String email = "ddang@pusan.ac.kr"; - final boolean isDuplicate = false; - final CheckDuplicateResponse response = new CheckDuplicateResponse(isDuplicate); - - //when - when(authSignUpService.isDuplicateEmail(email)).thenReturn(response); - - //then - mockMvc.perform(RestDocumentationRequestBuilders.get("/sign-up/exists/email") - .param("email", email)) - .andExpect(status().isOk()) - .andDo(document("auth-check-duplicate-email", queryParameters, responseBodySnippet)); - } - @Test @DisplayName("[성공] 중복하는 학번인지 확인할 수 있다.") public void checkDuplicateStudentId() throws Exception { @@ -146,28 +122,4 @@ public void checkDuplicateStudentId() throws Exception { .andExpect(status().isOk()) .andDo(document("auth-check-duplicate-student-id", queryParameters, responseBodySnippet)); } - - @Test - @DisplayName("[성공] 중복하는 전화번호인지 확인할 수 있다.") - public void checkDuplicatePhoneNumber() throws Exception { - // given - final QueryParametersSnippet queryParameters = queryParameters( - parameterWithName("phone_number").description("전화번호") - ); - final ResponseFieldsSnippet responseBodySnippet = responseFields( - fieldWithPath("is_duplicate").type(JsonFieldType.BOOLEAN).description("중복 여부")); - - final String phoneNumber = "01012341234"; - final boolean isDuplicate = false; - final CheckDuplicateResponse response = new CheckDuplicateResponse(isDuplicate); - - //when - when(authSignUpService.isDuplicatePhoneNumber(phoneNumber)).thenReturn(response); - - //then - mockMvc.perform(RestDocumentationRequestBuilders.get("/sign-up/exists/phone-number") - .param("phone_number", phoneNumber)) - .andExpect(status().isOk()) - .andDo(document("auth-check-duplicate-phone-number", queryParameters, responseBodySnippet)); - } } diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminAuthApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminAuthApiDocsTest.java new file mode 100644 index 00000000..72eccf52 --- /dev/null +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminAuthApiDocsTest.java @@ -0,0 +1,109 @@ +package sw_css.restdocs.docs.admin; + +import static org.apache.tomcat.util.http.fileupload.FileUploadBase.MULTIPART_FORM_DATA; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.RequestFieldsSnippet; +import org.springframework.restdocs.request.RequestPartsSnippet; +import sw_css.admin.auth.api.AdminAuthController; +import sw_css.admin.auth.application.dto.request.DeleteFacultyRequest; +import sw_css.admin.auth.application.dto.request.RegisterFacultyRequest; +import sw_css.restdocs.RestDocsTest; + +@WebMvcTest(AdminAuthController.class) +public class AdminAuthApiDocsTest extends RestDocsTest { + @Test + @DisplayName("[성공] 관리자 단일 등록을 할 수 있다.") + public void registerFacultyMemberInfo() throws Exception { + // given + final RequestFieldsSnippet requestFieldsSnippet = requestFields( + fieldWithPath("email").type(JsonFieldType.STRING).description("부산대학교 이메일"), + fieldWithPath("name").type(JsonFieldType.STRING).description("실명") + ); + + final String email = "root@pusan.ac.kr"; + final String name = "관리자"; + RegisterFacultyRequest request = new RegisterFacultyRequest(email, name); + + final String token = "Bearer AccessToken"; + + // when + when(adminAuthCommandService.registerFaculty(request)).thenReturn(1L); + + // then + mockMvc.perform(post("/admin/auth") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isCreated()) + .andDo(document("admin-auth-register", requestFieldsSnippet)); + } + + @Test + @DisplayName("[성공] 관리자 다중 등록을 할 수 있다.") + public void registerFaculties() throws Exception { + // given + final RequestPartsSnippet requestPartsSnippet = requestParts( + partWithName("file").description("일괄 등록할 관리자 이메일 및 이름이 담긴 엑셀 파일(.xls, .xlsx)") + ); + + final MockMultipartFile request = new MockMultipartFile("file", "test.xls", "multipart/form-data", + "example".getBytes()); + + final String token = "Bearer AccessToken"; + + // when + doNothing().when(adminAuthCommandService).registerFaculties(request); + + // then + mockMvc.perform(multipart("/admin/auth/files").file(request) + .contentType(MULTIPART_FORM_DATA) + .accept(APPLICATION_JSON) + .characterEncoding("UTF-8") + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isCreated()) + .andDo(document("admin-auth-register-by-file", requestPartsSnippet)); + } + + @Test + @DisplayName("[성공] 관리자 삭제할 수 있다.") + public void deleteFaculty() throws Exception { + // given + final RequestFieldsSnippet requestFieldsSnippet = requestFields( + fieldWithPath("member_id").type(JsonFieldType.NUMBER).description("관리자의 id") + ); + + final long faculty_id = 1L; + DeleteFacultyRequest request = new DeleteFacultyRequest(faculty_id); + + final String token = "Bearer AccessToken"; + + // when + doNothing().when(adminAuthCommandService).deleteFaculty(request.member_id()); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.delete("/admin/auth") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()) + .andDo(document("admin-auth-delete", requestFieldsSnippet)); + } +}