Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 이메일 인증&검증 #94

Merged
merged 10 commits into from
Jun 16, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,26 @@
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Tag(name = "Auth", description = "인증 관련 API")
@Tag(name = "Auth", description = "인증 관련 API [김은채]")
public class AuthController {

private final AuthService authService;

@PostMapping("/signup")
@Operation(summary = "구직자 회원 가입", description = "새로운 사용자를 등록합니다. 구직자 회원가입입니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "회원 가입 성공",
@ApiResponse(responseCode = "201", description = "구직자 회원 가입 성공",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"200\", \"message\": \"회원 가입 성공\", \"data\": null }"))),
@ApiResponse(responseCode = "400", description = "회원 가입 실패",
examples = @ExampleObject(value = "{ \"code\": \"201\", \"message\": \"구직자 회원 가입 성공\", \"data\": null }"))),
@ApiResponse(responseCode = "400", description = "유효성 검사 실패",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"400\", \"message\": \"회원 가입 실패\", \"data\": null }"))),
examples = @ExampleObject(value = "{ \"code\": \"400\", \"message\": \"유효성 검사 실패\", \"data\": null }"))),
@ApiResponse(responseCode = "400", description = "이메일 인증 필요",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"400\", \"message\": \"이메일 인증 필요\", \"data\": null }"))),
@ApiResponse(responseCode = "409", description = "중복된 이메일",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"409\", \"message\": \"중복된 이메일\", \"data\": null }"))),
@ApiResponse(responseCode = "500", description = "서버 에러",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"500\", \"message\": \"서버 에러\", \"data\": null }")))
Expand All @@ -60,14 +66,20 @@ public ResponseEntity<GlobalApiResponse<Object>> signup(
}

@PostMapping("/cp-signup")
@Operation(summary = "기업 회원 가입", description = "새로운 기업 사용자를 등록합니다. 기업/구직자 회원가입입니다.")
@Operation(summary = "기업 회원 가입", description = "새로운 기업 사용자를 등록합니다. 기업(회원) 회원가입입니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "회원 가입 성공",
@ApiResponse(responseCode = "201", description = "기업 회원 가입 성공",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"201\", \"message\": \"기업 회원 가입 성공\", \"data\": null }"))),
@ApiResponse(responseCode = "400", description = "유효성 검사 실패",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"400\", \"message\": \"유효성 검사 실패\", \"data\": null }"))),
@ApiResponse(responseCode = "400", description = "이메일 인증 필요",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"200\", \"message\": \"회원 가입 성공\", \"data\": null }"))),
@ApiResponse(responseCode = "400", description = "회원 가입 실패",
examples = @ExampleObject(value = "{ \"code\": \"400\", \"message\": \"이메일 인증 필요\", \"data\": null }"))),
@ApiResponse(responseCode = "409", description = "중복된 이메일",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"400\", \"message\": \"회원 가입 실패\", \"data\": null }"))),
examples = @ExampleObject(value = "{ \"code\": \"409\", \"message\": \"중복된 이메일\", \"data\": null }"))),
@ApiResponse(responseCode = "500", description = "서버 에러",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"500\", \"message\": \"서버 에러\", \"data\": null }")))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import team9502.sinchulgwinong.domain.companyUser.entity.CompanyUser;
import team9502.sinchulgwinong.domain.companyUser.repository.CompanyUserRepository;
import team9502.sinchulgwinong.domain.companyUser.service.EncryptionService;
import team9502.sinchulgwinong.domain.email.service.EmailVerificationService;
import team9502.sinchulgwinong.domain.point.enums.SpType;
import team9502.sinchulgwinong.domain.point.service.PointService;
import team9502.sinchulgwinong.domain.user.entity.User;
Expand All @@ -40,12 +41,18 @@ public class AuthService {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final PointService pointService;
private final EmailVerificationService emailVerificationService;

@Transactional
public void signup(UserSignupRequestDTO signupRequest) {

validateUserSignupRequest(signupRequest.getEmail(), signupRequest.getPassword(),
signupRequest.getConfirmPassword(), signupRequest.isAgreeToTerms());

if (!emailVerificationService.isEmailVerified(signupRequest.getEmail())) {
throw new ApiException(ErrorCode.EMAIL_NOT_VERIFIED);
}

try {
User user = User.builder()
.username(signupRequest.getUsername())
Expand All @@ -66,9 +73,14 @@ public void signup(UserSignupRequestDTO signupRequest) {

@Transactional
public void cpSignup(CpUserSignupRequestDTO requestDTO) {

validateCpSignupRequest(requestDTO.getCpEmail(), requestDTO.getCpPassword(),
requestDTO.getCpConfirmPassword(), requestDTO.isAgreeToTerms());

if (!emailVerificationService.isEmailVerified(requestDTO.getCpEmail())) {
throw new ApiException(ErrorCode.EMAIL_NOT_VERIFIED);
}

try {
CompanyUser companyUser = CompanyUser.builder()
.hiringStatus(requestDTO.getHiringStatus())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ public ResponseEntity<GlobalApiResponse<CpUserProfileResponseDTO>> getCompanyUse
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"400\", \"message\": \"요청 데이터가 유효하지 않습니다.\", \"data\": null }"))),
@ApiResponse(responseCode = "400", description = "이메일 인증 필요",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"400\", \"message\": \"이메일 인증이 필요합니다.\", \"data\": null }"))),
@ApiResponse(responseCode = "404", description = "기업(회원)을 찾을 수 없음",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"404\", \"message\": \"기업(회원)을 찾을 수 없습니다.\", \"data\": null }"))),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package team9502.sinchulgwinong.domain.companyUser.service;


import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
Expand All @@ -10,6 +9,7 @@
import team9502.sinchulgwinong.domain.companyUser.dto.response.CpUserProfileResponseDTO;
import team9502.sinchulgwinong.domain.companyUser.entity.CompanyUser;
import team9502.sinchulgwinong.domain.companyUser.repository.CompanyUserRepository;
import team9502.sinchulgwinong.domain.email.service.EmailVerificationService;
import team9502.sinchulgwinong.global.exception.ApiException;
import team9502.sinchulgwinong.global.exception.ErrorCode;

Expand All @@ -20,6 +20,7 @@ public class CpUserService {
private final CompanyUserRepository companyUserRepository;
private final EncryptionService encryptionService;
private final PasswordEncoder passwordEncoder;
private final EmailVerificationService emailVerificationService;

@Transactional(readOnly = true)
public CpUserProfileResponseDTO getCpUserProfile(Long cpUserId) {
Expand Down Expand Up @@ -59,6 +60,9 @@ public CpUserProfileResponseDTO updateCpUserProfile(Long cpUserId, CpUserProfile
companyUser.setDescription(updateRequestDTO.getDescription());
}
if (updateRequestDTO.getCpEmail() != null) {
if (!emailVerificationService.isEmailVerified(updateRequestDTO.getCpEmail())) {
throw new ApiException(ErrorCode.EMAIL_NOT_VERIFIED);
}
companyUser.setCpEmail(updateRequestDTO.getCpEmail());
}
if (updateRequestDTO.getCpPhoneNumber() != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package team9502.sinchulgwinong.domain.email.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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 team9502.sinchulgwinong.domain.email.dto.request.EmailVerificationCodeRequestDTO;
import team9502.sinchulgwinong.domain.email.dto.request.EmailVerificationRequestDTO;
import team9502.sinchulgwinong.domain.email.service.EmailVerificationService;
import team9502.sinchulgwinong.global.response.GlobalApiResponse;

import static team9502.sinchulgwinong.global.response.SuccessCode.SUCCESS_EMAIL_VERIFICATION;
import static team9502.sinchulgwinong.global.response.SuccessCode.SUCCESS_EMAIL_VERIFICATION_SENT;

@RestController
@RequiredArgsConstructor
@RequestMapping("/email")
@Tag(name = "Email Verification", description = "이메일 인증 관련 API [김은채]")
public class EmailVerificationController {

private final EmailVerificationService emailVerificationService;

@PostMapping("/sendCode")
@Operation(summary = "이메일 인증 코드 발송", description = "사용자 이메일로 인증 코드를 발송합니다.")
@ApiResponse(responseCode = "200", description = "인증 코드 발송 성공",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"200\", \"message\": \"인증 코드 발송 성공\", \"data\": null }")))
@ApiResponse(responseCode = "400", description = "잘못된 입력",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"400\", \"message\": \"잘못된 입력입니다.\", \"data\": null }")))
@ApiResponse(responseCode = "401", description = "이메일 인증 실패",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"401\", \"message\": \"이메일 인증에 실패했습니다.\", \"data\": null }")))
@ApiResponse(responseCode = "500", description = "서버 에러",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"500\", \"message\": \"서버 에러\", \"data\": null }")))
public ResponseEntity<GlobalApiResponse<Void>> sendVerificationCode(
@RequestBody EmailVerificationRequestDTO requestDTO) {

emailVerificationService.createVerification(requestDTO.getEmail(), requestDTO.getUserType());

return ResponseEntity.status(SUCCESS_EMAIL_VERIFICATION_SENT.getHttpStatus())
.body(
GlobalApiResponse.of(
SUCCESS_EMAIL_VERIFICATION_SENT.getMessage(),
null
)
);
}

@PostMapping("/verifyCode")
@Operation(summary = "이메일 인증 코드 검증", description = "제공된 이메일과 인증 코드의 유효성을 검증합니다.")
@ApiResponse(responseCode = "200", description = "이메일 인증 성공",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"200\", \"message\": \"이메일 인증 성공\", \"data\": null }")))
@ApiResponse(responseCode = "400", description = "유효하지 않거나 만료된 코드",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"400\", \"message\": \"유효하지 않거나 만료된 인증 코드\", \"data\": null }")))
@ApiResponse(responseCode = "401", description = "잘못된 입력",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"400\", \"message\": \"잘못된 입력입니다.\", \"data\": null }")))
@ApiResponse(responseCode = "500", description = "서버 에러",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "{ \"code\": \"500\", \"message\": \"서버 에러\", \"data\": null }")))
public ResponseEntity<GlobalApiResponse<Void>> verifyCode(
@RequestBody EmailVerificationCodeRequestDTO requestDTO) {

emailVerificationService.verifyCode(requestDTO.getEmail(), requestDTO.getCode());


return ResponseEntity.status(SUCCESS_EMAIL_VERIFICATION.getHttpStatus())
.body(
GlobalApiResponse.of(
SUCCESS_EMAIL_VERIFICATION.getMessage(),
null
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package team9502.sinchulgwinong.domain.email.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Data
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class EmailVerificationCodeRequestDTO {

@Schema(description = "이메일 주소", example = "[email protected]")
private String email;

@Schema(description = "인증 코드", example = "123456")
private String code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package team9502.sinchulgwinong.domain.email.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import team9502.sinchulgwinong.domain.email.enums.UserType;

@Data
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class EmailVerificationRequestDTO {

@Schema(description = "이메일 주소", example = "[email protected]")
private String email;

@Schema(description = "사용자 유형", example = "USER")
private UserType userType;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package team9502.sinchulgwinong.domain.email.entity;

import jakarta.persistence.*;
import lombok.*;
import team9502.sinchulgwinong.domain.email.enums.UserType;
import team9502.sinchulgwinong.domain.email.enums.VerificationStatus;

import java.util.Date;

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "EmailVerifications", indexes = {
@Index(name = "idx_expires_at_status", columnList = "expiresAt, status")
})
public class EmailVerifications {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long verificationId;

@Column(nullable = false, length = 250)
private String email;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private UserType userType;

@Column
private Long userReferenceId;

@Column(nullable = false, length = 6)
private String verificationCode;

@Column(nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;

@Column(nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private Date expiresAt;

@Setter
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private VerificationStatus status;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package team9502.sinchulgwinong.domain.email.enums;

public enum UserType {

USER, CPUSER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package team9502.sinchulgwinong.domain.email.enums;

public enum VerificationStatus {

PENDING, VERIFIED, EXPIRED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package team9502.sinchulgwinong.domain.email.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import team9502.sinchulgwinong.domain.email.entity.EmailVerifications;
import team9502.sinchulgwinong.domain.email.enums.VerificationStatus;

import java.util.Date;
import java.util.List;
import java.util.Optional;

public interface EmailVerificationRepository extends JpaRepository<EmailVerifications, Long> {

Optional<EmailVerifications> findByEmailAndVerificationCodeAndExpiresAtAfterAndStatus(String email, String verificationCode, Date now, VerificationStatus status);

List<EmailVerifications> findByExpiresAtBeforeAndStatus(Date expiresAt, VerificationStatus status);

void deleteByExpiresAtBeforeAndStatus(Date expiresAt, VerificationStatus status);

boolean existsByEmailAndStatus(String email, VerificationStatus status);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package team9502.sinchulgwinong.domain.email.service;

public interface EmailService {

void sendSimpleMessage(String to, String subject, String text);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package team9502.sinchulgwinong.domain.email.service;

import lombok.RequiredArgsConstructor;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class EmailServiceImpl implements EmailService {

private final JavaMailSender mailSender;

@Override
public void sendSimpleMessage(String to, String subject, String text) {

SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("${GOOGLE_ACCOUNT}");
message.setTo(to);
message.setSubject(subject);
message.setText(text);
mailSender.send(message);
}
}
Loading