[Practice] 메일 전송 (2)

2021. 4. 20. 15:09Spring/Practice

1. 문제

  • 비밀번호 초기화를 위해 이메일로 인증코드를 전송하는 API

 

 

 

2. 풀이

- schema.sql

create table USER (
    ID          BIGINT auto_increment primary key,
    EMAIL       VARCHAR(255),
    NAME        VARCHAR(255),
    PASSWORD    VARCHAR(255),
    PHONE       VARCHAR(255),
    REG_DATE    TIMESTAMP,
    UPDATE_DATE TIMESTAMP,
    STATUS      INTEGER,
    LOCK_YN     BOOLEAN DEFAULT FALSE,

    PASSWORD_RESET_YN   BOOLEAN DEFAULT FALSE,
    PASSWORD_RESET_KEY  VARCHAR(255)
);

...

create table MAIL_TEMPLATE
(
    ID             BIGINT auto_increment primary key,
    TEMPLATE_ID    VARCHAR(255),
    TITLE          VARCHAR(255),
    CONTENTS       VARCHAR(255),
    SEND_EMAIL     VARCHAR(255),
    SEND_USER_NAME VARCHAR(255),
    REG_DATE       TIMESTAMP
);

- data.sql

INSERT INTO USER(ID, EMAIL, PASSWORD, PHONE, REG_DATE, UPDATE_DATE, NAME, STATUS, LOCK_YN, PASSWORD_RESET_YN) VALUES(1, 'test1@naver.com', '1111', '010-1111-1111', '2021-01-01 01:01:01.000000', null, '테스트1', 1, 0, 0);
INSERT INTO USER(ID, EMAIL, PASSWORD, PHONE, REG_DATE, UPDATE_DATE, NAME, STATUS, LOCK_YN, PASSWORD_RESET_YN) VALUES(2, 'test2@naver.com', '2222', '010-2222-2222', '2021-01-02 02:02:02.000000', null, '테스트2', 1, 0, 0);
INSERT INTO USER(ID, EMAIL, PASSWORD, PHONE, REG_DATE, UPDATE_DATE, NAME, STATUS, LOCK_YN, PASSWORD_RESET_YN) VALUES(3, 'test3@naver.com', '3333', '010-3333-3333', '2021-01-03 03:03:03.000000', null, '테스트3', 2, 0, 0);

...

INSERT INTO MAIL_TEMPLATE(ID, TEMPLATE_ID, TITLE, CONTENTS, SEND_EMAIL, SEND_USER_NAME, REG_DATE) VALUES
    (1, 'USER_RESET_PASSWORD', '{USER_NAME}님의 비밀번호 초기화 요청입니다.', '<div><p>{USER_NAME}님 안녕하세요.</p><p>아래 링크를 클릭하여, 비밀번호를 초기화해주세요.</p><p><a href="{SERVER_URL}/reset?key={RESET_PASSWORD_KEY}">초기화</a></p></div>',
    'ozofweird@gmail.com', '관리자', '2021-01-01 01:01:01.000000');

- User.java

package com.example.jpa.sample.user.entity;

import com.example.jpa.sample.user.model.UserStatus;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
@Entity
public class User {

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

    @Column
    private String email;

    @Column
    private String name;

    @Column
    private String password;

    @Column
    private String phone;

    @Column
    private LocalDateTime regDate;

    @Column
    private LocalDateTime updateDate;

    @Column
    private UserStatus status;

    @Column
    private boolean lockYn;

    @Column // 사용자가 패스워드 초기화 요청을 했는지
    private boolean passwordResetYn;

    @Column
    private String passwordResetKey;
}

- UserService.java

package com.example.jpa.sample.user.service;

import com.example.jpa.sample.board.model.ServiceResult;
import com.example.jpa.sample.user.entity.User;
import com.example.jpa.sample.user.model.*;

import java.util.List;

public interface UserService {

    ...

    ServiceResult resetPassword(UserPasswordResetInput userPasswordResetInput);
}

- UserServiceImpl.java

package com.example.jpa.sample.user.service;

import com.example.jpa.sample.board.model.ServiceResult;
import com.example.jpa.sample.common.MailComponent;
import com.example.jpa.sample.common.exception.BizException;
import com.example.jpa.sample.mail.entity.MailTemplate;
import com.example.jpa.sample.mail.repository.MailTemplateRepository;
import com.example.jpa.sample.user.entity.User;
import com.example.jpa.sample.user.entity.UserInterest;
import com.example.jpa.sample.user.model.*;
import com.example.jpa.sample.user.repository.UserCustomRepository;
import com.example.jpa.sample.user.repository.UserInterestRepository;
import com.example.jpa.sample.user.repository.UserRepository;
import com.example.jpa.sample.util.PasswordUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@RequiredArgsConstructor
@Service
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final UserCustomRepository userCustomRepository;
    private final UserInterestRepository userInterestRepository;

    private final MailComponent mailComponent;
    private final MailTemplateRepository mailTemplateRepository;

    ...

    @Override
    public ServiceResult resetPassword(UserPasswordResetInput userPasswordResetInput) {

        Optional<User> optionalUser = userRepository.findByEmailAndName(userPasswordResetInput.getEmail(), userPasswordResetInput.getName());
        if(!optionalUser.isPresent()) {
            throw new BizException("회원정보가 존재하지 않습니다.");
        }
        User user = optionalUser.get();

        String passwordResetKey = UUID.randomUUID().toString();

        user.setPasswordResetYn(true);
        user.setPasswordResetKey(passwordResetKey);
        userRepository.save(user);

        String serverUrl = "http://localhost:8080";

        Optional<MailTemplate> optionalMailTemplate = mailTemplateRepository.findByTemplateId("USER_RESET_PASSWORD");
        optionalMailTemplate.ifPresent(e -> {

            String fromEmail = e.getSendEmail();
            String fromName = e.getSendUserName();

            String title = e.getTitle().replaceAll("\\{USER_NAME\\}", user.getName());
            String contents = e.getContents().replaceAll("\\{USER_NAME\\}", user.getName())
                    .replaceAll("\\{SERVER_URL\\}", serverUrl)
                    .replaceAll("\\{RESET_PASSWORD_KEY\\}", passwordResetKey);

            mailComponent.send(fromEmail, fromName, user.getEmail(), user.getName(), title, contents);

        });

        return ServiceResult.success();
    }
}

- UserPasswordResetInput.java

package com.example.jpa.sample.user.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class UserPasswordResetInput {

    @Email(message = "이메일 형식이 아닙니다.")
    @NotBlank(message = "이메일은 필수 입력 사항입니다.")
    private String email;

    @NotBlank(message = "이름은 필수 입력 사항입니다.")
    private String name;
}

- user_reset_password.html (resource - mail-template)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{USER_NAME}님의 비밀번호 초기화 요청입니다.</title>
</head>
<body>
    <div>
        <p>{USER_NAME}님 안녕하세요.</p>
        <p>아래 링크를 클릭하여, 비밀번호를 초기화해주세요.</p>
        <p>
            <a href="{SERVER_URL}/reset?key={RESET_PASSWORD_KEY}">초기화</a>
        </p>
    </div>
</body>
</html>

- MailTemplate.java

package com.example.jpa.sample.mail.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
@Entity
public class MailTemplate {

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

    @Column
    private String templateId;

    @Column
    private String title;

    @Column
    private String contents;

    @Column
    private String sendEmail;

    @Column
    private String sendUserName;

    @Column
    private LocalDateTime regDate;
}

- MailTemplateRepository.java

package com.example.jpa.sample.mail.repository;

import com.example.jpa.sample.mail.entity.MailTemplate;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MailTemplateRepository extends JpaRepository<MailTemplate, Long> {

    Optional<MailTemplate> findByTemplateId(String templateId);
}

- ApiUserController.java

package com.example.jpa.sample.user.controller;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.example.jpa.sample.board.entity.Board;
import com.example.jpa.sample.board.entity.BoardComment;
import com.example.jpa.sample.board.model.ServiceResult;
import com.example.jpa.sample.board.service.BoardService;
import com.example.jpa.sample.common.exception.BizException;
import com.example.jpa.sample.common.model.ResponseResult;
import com.example.jpa.sample.notice.entity.Notice;
import com.example.jpa.sample.notice.entity.NoticeLike;
import com.example.jpa.sample.notice.model.NoticeResponse;
import com.example.jpa.sample.notice.model.ResponseError;
import com.example.jpa.sample.notice.repository.NoticeLikeRepository;
import com.example.jpa.sample.notice.repository.NoticeRepository;
import com.example.jpa.sample.user.entity.User;
import com.example.jpa.sample.user.exception.ExistsEmailException;
import com.example.jpa.sample.user.exception.PasswordNotMatchException;
import com.example.jpa.sample.user.exception.UserNotFoundException;
import com.example.jpa.sample.user.model.*;
import com.example.jpa.sample.user.repository.UserPointRepository;
import com.example.jpa.sample.user.repository.UserRepository;
import com.example.jpa.sample.user.service.UserPointService;
import com.example.jpa.sample.user.service.UserService;
import com.example.jpa.sample.util.JwtUtils;
import com.example.jpa.sample.util.PasswordUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;

@RequiredArgsConstructor
@RestController
public class ApiUserController {

    private final UserRepository userRepository;
    private final NoticeRepository noticeRepository;
    private final NoticeLikeRepository noticeLikeRepository;

    private final BoardService boardService;
    private final UserPointService userPointService;

    private final UserService userService;

    ...

    //------------------------------------------------------------------------------
    // 문제 1
    @PostMapping("/api/public/user") // public url에 대해서는 인터셉터가 거치지 않음
    public ResponseEntity<?> addUser(@RequestBody UserInput userInput) {
        ServiceResult result = userService.addUser(userInput);
        return ResponseResult.success(result);
    }

    // 문제 2
    @PostMapping("/api/public/user/password/reset")
    public ResponseEntity<?> resetPassword(@RequestBody @Valid UserPasswordResetInput userPasswordResetInput, Errors errors) {

        if(errors.hasErrors()) {
            return ResponseResult.fail("입력값이 정확하지 않습니다.", ResponseError.of(errors.getAllErrors()));
        }

        ServiceResult result = null;
        try {
            result = userService.resetPassword(userPasswordResetInput);

        } catch (BizException e) {
            return ResponseResult.fail(e.getMessage());
        }
        return ResponseResult.success(result);
    }

}
728x90

'Spring > Practice' 카테고리의 다른 글

[Practice] 메일 전송 (4)  (0) 2021.04.20
[Practice] 메일 전송 (3)  (0) 2021.04.20
[Practice] 메일 전송 (1)  (0) 2021.04.20
[Practice] Open API 연동 시 API 프로퍼티 활용  (0) 2021.04.20