Skip to content

Commit

Permalink
Merge pull request #42 from Tico-Corp/feat-be/TICO-222-create-admin-api
Browse files Browse the repository at this point in the history
[FEAT] 관리자 회원 가입 및 로그인 API 구현 (TICO-222)
  • Loading branch information
bu119 authored Jul 16, 2024
2 parents f6be641 + d6043e8 commit 33f0734
Show file tree
Hide file tree
Showing 15 changed files with 428 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.tico.pomoro_do.domain.user.controller;

import com.tico.pomoro_do.domain.user.dto.request.AdminJoinDTO;
import com.tico.pomoro_do.domain.user.dto.request.AdminLoginDTO;
import com.tico.pomoro_do.domain.user.dto.response.JwtDTO;
import com.tico.pomoro_do.domain.user.service.AdminService;
import com.tico.pomoro_do.global.base.CustomSuccessCode;
import com.tico.pomoro_do.global.base.SuccessResponseDTO;
import com.tico.pomoro_do.global.exception.ErrorResponseEntity;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.security.GeneralSecurityException;

@Tag(name = "admin: 관리자", description = "백엔드를 테스트를 위한 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/admin")
@Slf4j
public class AdminController {
//백엔드에서는 구글 로그인이 불가하므로 생성함.
//관리자 로그인
//관리자 로그아웃

private final AdminService adminService;

/**
* 관리자 회원가입 API
*
* @param request AdminJoinDTO 객체
* @return 성공 시 JwtDTO를 포함하는 SuccessResponseDTO
*/
@Operation(
summary = "관리자 회원가입",
description = "관리자 회원가입을 수행합니다. <br>"
+ "관리자의 이메일은 @pomorodo.shop 도메인으로 제한됩니다. <br>"
+ "성공 시에는 JwtDTO를 포함하는 SuccessResponseDTO를 반환합니다.",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "AdminJoinDTO 객체",
required = true,
content = @Content(schema = @Schema(implementation = AdminJoinDTO.class))
)
)
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "회원가입 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class))),
@ApiResponse(responseCode = "409", description = "이미 등록된 사용자",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class)))
})
@PostMapping("/join")
public ResponseEntity<SuccessResponseDTO<JwtDTO>> adminJoin(@RequestBody AdminJoinDTO request) {
log.info("관리자 회원가입 요청: {}", request.getUsername());
JwtDTO jwtResponse = adminService.adminJoin(request);
SuccessResponseDTO<JwtDTO> response = SuccessResponseDTO.<JwtDTO>builder()
.status(CustomSuccessCode.ADMIN_SIGNUP_SUCCESS.getHttpStatus().value())
.message(CustomSuccessCode.ADMIN_SIGNUP_SUCCESS.getMessage())
.data(jwtResponse)
.build();
log.info("관리자 회원가입 성공: {}", request.getUsername());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

/**
* 관리자 로그인 API
*
* @param request AdminLoginDTO 객체
* @return 성공 시 JwtDTO를 포함하는 SuccessResponseDTO
*/
@Operation(
summary = "관리자 로그인",
description = "관리자 로그인을 수행합니다. <br>"
+ "성공 시에는 JwtDTO를 포함하는 SuccessResponseDTO를 반환합니다.",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "AdminLoginDTO 객체",
required = true,
content = @Content(schema = @Schema(implementation = AdminLoginDTO.class))
)
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "로그인 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class))),
@ApiResponse(responseCode = "404", description = "등록되지 않은 사용자",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class))),
@ApiResponse(responseCode = "403", description = "관리자 권한이 없음",
content = @Content(schema = @Schema(implementation = ErrorResponseEntity.class)))
})
@PostMapping("/login")
public ResponseEntity<SuccessResponseDTO<JwtDTO>> adminLogin(@RequestBody AdminLoginDTO request) {
log.info("관리자 로그인 요청: {}", request.getUsername());
JwtDTO jwtResponse = adminService.adminLogin(request);
SuccessResponseDTO<JwtDTO> response = SuccessResponseDTO.<JwtDTO>builder()
.status(CustomSuccessCode.ADMIN_LOGIN_SUCCESS.getHttpStatus().value())
.message(CustomSuccessCode.ADMIN_LOGIN_SUCCESS.getMessage())
.data(jwtResponse)
.build();
log.info("관리자 로그인 성공: {}", request.getUsername());
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
@Tag(name = "Auth: 인증", description = "인증 관련 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("api/auth")
@RequestMapping("/api/auth")
public class AuthController {
//사용자 로그인
//사용자 로그아웃
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.tico.pomoro_do.domain.user.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;

@Getter
@Schema(description = "Admin Join Info")
public class AdminJoinDTO {

@Email
@NotBlank(message = "이메일을 입력해주세요.")
private String username;
@NotBlank(message = "닉네임을 입력해주세요.")
private String nickname;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.tico.pomoro_do.domain.user.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;

@Getter
@Schema(description = "Admin login Info")
public class AdminLoginDTO {

@Email
@NotBlank(message = "이메일을 입력해주세요.")
private String username;
@NotBlank(message = "닉네임을 입력해주세요.")
private String nickname;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class User {
// 활성화 상태
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserStatus status;
private UserStatus status = UserStatus.ACTIVE;

@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt = LocalDateTime.now();
Expand All @@ -53,12 +53,11 @@ protected void onUpdate() {
}

@Builder
public User(String username, String nickname, String profileImageUrl, UserRole role, UserStatus status) {
public User(String username, String nickname, String profileImageUrl, UserRole role) {
this.username = username;
this.nickname = nickname;
this.profileImageUrl = profileImageUrl;
this.role = role;
this.status = status;
}

public void updateNickname(String nickname) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.tico.pomoro_do.domain.user.service;

import com.tico.pomoro_do.domain.user.dto.request.AdminJoinDTO;
import com.tico.pomoro_do.domain.user.dto.request.AdminLoginDTO;
import com.tico.pomoro_do.domain.user.dto.response.JwtDTO;

public interface AdminService {

//관리자 회원가입
JwtDTO adminJoin(AdminJoinDTO adminJoinDTO);

//관리자 로그인
JwtDTO adminLogin(AdminLoginDTO adminLoginDTO);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.tico.pomoro_do.domain.user.service;

import com.tico.pomoro_do.domain.user.dto.request.AdminJoinDTO;
import com.tico.pomoro_do.domain.user.dto.request.AdminLoginDTO;
import com.tico.pomoro_do.domain.user.dto.response.JwtDTO;
import com.tico.pomoro_do.domain.user.entity.User;
import com.tico.pomoro_do.domain.user.repository.UserRepository;
import com.tico.pomoro_do.global.common.enums.UserRole;
import com.tico.pomoro_do.global.exception.CustomErrorCode;
import com.tico.pomoro_do.global.exception.CustomException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@Transactional(readOnly = true) // (성능 최적화 - 읽기 전용에만 사용)
@RequiredArgsConstructor // 파이널 필드만 가지고 생성사 주입 함수 만듬 (따로 작성할 필요 없다.)
@Slf4j
public class AdminServiceImpl implements AdminService {

private final UserRepository userRepository;
private final AuthService authService;

private static final String ADMIN_EMAIL_DOMAIN = "pomorodo.shop";

/**
* 관리자 회원가입 처리
*
* @param adminJoinDTO AdminJoinDTO 객체
* @return JwtDTO를 포함하는 ResponseEntity
* @throws CustomException 이메일 도메인이 유효하지 않거나 이미 등록된 사용자인 경우 예외
*/
@Override
@Transactional
public JwtDTO adminJoin(AdminJoinDTO adminJoinDTO) {
String username = adminJoinDTO.getUsername();
String nickname = adminJoinDTO.getNickname();

//관리자 회원가입 도메인 가져오기
String domain = getEmailDomain(username);
//관리자 회원가입 이메일 도메인 검증
validateAdminEmailDomain(domain);
//관리자 이메일 가입 여부 검증
checkUserExistence(username);

// 관리자 생성하기
User admin = authService.createUser(username, nickname, "", UserRole.ADMIN);

return authService.createJwtTokens(username, String.valueOf(UserRole.ADMIN));
}

/**
* 관리자 로그인 처리
*
* @param adminLoginDTO AdminLoginDTO 객체
* @return JwtDTO를 포함하는 ResponseEntity
* @throws CustomException 이메일 도메인이 유효하지 않거나 관리자가 아닌 경우 예외
*/
@Override
public JwtDTO adminLogin(AdminLoginDTO adminLoginDTO){
String username = adminLoginDTO.getUsername();
String nickname = adminLoginDTO.getNickname();

//관리자 로그인 도메인 가져오기
String domain = getEmailDomain(username);
// 관리자 로그인 이메일 도메인 검증
validateAdminEmailDomain(domain);
// 관리자 로그인 검증
validateAdminUser(username, nickname);
return authService.createJwtTokens(username, String.valueOf(UserRole.ADMIN));
}

/**
* 이메일에서 도메인 부분을 추출
*
* @param email 이메일 주소
* @return 이메일 도메인 부분
*/
private String getEmailDomain(String email) {
return email.substring(email.indexOf("@") + 1);
}

/**
* 이메일 도메인 검증
*
* @param domain 이메일 도메인
* @throws CustomException 유효하지 않은 이메일 도메인의 경우 예외 발생
*/
private void validateAdminEmailDomain(String domain) {
if (!ADMIN_EMAIL_DOMAIN.equals(domain)) {
log.error("유효하지 않은 이메일 도메인: {}", domain);
throw new CustomException(CustomErrorCode.ADMIN_EMAIL_ONLY);
}
}

/**
* 사용자가 이미 존재하는지 확인
*
* @param username 사용자 이름
* @throws CustomException 이미 등록된 사용자인 경우 예외 발생
*/
private void checkUserExistence(String username) {
if (userRepository.existsByUsername(username)) {
log.error("이미 등록된 사용자: {}", username);
throw new CustomException(CustomErrorCode.USER_ALREADY_REGISTERED);
}
}

/**
* 관리자 검증
*
* @param username 사용자 이름
* @param nickname 사용자 닉네임
* @throws CustomException 사용자가 존재하지 않거나 관리자가 아닌 경우 예외 발생
*/
private void validateAdminUser(String username, String nickname) {
Optional<User> userData = userRepository.findByUsername(username);
if (userData.isEmpty()) {
log.error("사용자를 찾을 수 없음: {}", username);
throw new CustomException(CustomErrorCode.USER_NOT_FOUND);
}
User admin = userData.get();
if (!admin.getRole().equals(UserRole.ADMIN)) {
log.error("관리자 권한 없음: {}", username);
throw new CustomException(CustomErrorCode.NOT_AN_ADMIN);
}
if (!admin.getNickname().equals(nickname)) {
log.error("닉네임 불일치: {}", username);
throw new CustomException(CustomErrorCode.ADMIN_LOGIN_FAILED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.tico.pomoro_do.domain.user.dto.GoogleUserInfoDTO;
import com.tico.pomoro_do.domain.user.dto.request.GoogleJoinDTO;
import com.tico.pomoro_do.domain.user.dto.response.JwtDTO;
import com.tico.pomoro_do.domain.user.entity.User;
import com.tico.pomoro_do.global.common.enums.UserRole;

import java.io.IOException;
import java.security.GeneralSecurityException;
Expand All @@ -19,6 +21,9 @@ public interface AuthService {
// 구글 회원가입
JwtDTO googleJoin(String authorizationHeader, GoogleJoinDTO request) throws GeneralSecurityException, IOException;

// User 생성
User createUser(String username, String nickname, String profileImageUrl, UserRole role);

// 토큰 생성
JwtDTO createJwtTokens(String email, String role);

Expand Down
Loading

0 comments on commit 33f0734

Please sign in to comment.