initial commit

This commit is contained in:
hscom96
2021-02-13 01:01:51 +09:00
parent f72a4b5cf5
commit 99d4e34ff9
108 changed files with 6240 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
package com.auth.userserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableCaching
@EnableEurekaClient
@SpringBootApplication
public class UserServerApplication {
public static void main(String[] args) {
SpringApplication.run(UserServerApplication.class, args);
}
}

View File

@@ -0,0 +1,18 @@
package com.auth.userserver.common.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@EnableResourceServer
@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();
http.authorizeRequests()
.antMatchers("/api/signup/**").permitAll()
.antMatchers("/api/user/**").hasAnyAuthority("ADMIN");
}
}

View File

@@ -0,0 +1,15 @@
package com.auth.userserver.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
@Configuration
public class WebConfig{
@Bean
public Pbkdf2PasswordEncoder getPasswordEncoder() {
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder();
pbkdf2PasswordEncoder.setAlgorithm(Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256);
return pbkdf2PasswordEncoder;
}
}

View File

@@ -0,0 +1,50 @@
package com.auth.userserver.common.constants;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
@AllArgsConstructor
public enum ErrorEnum {
/* 사용자 오류 메세지 정의 */
USER_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "USER_000", "해당 아이디가 이미 존재합니다"),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_001", "해당 아이디가 존재하지 않습니다."),
USER_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST, "USER_001", "해당 아이디가 이미 이메일 인증을 받았습니다."),
/* 사용자 오류 메세지 정의 끝*/
/* 기타 오류 메세지 정의 */
ETC(HttpStatus.INTERNAL_SERVER_ERROR, "ETC_000", "알 수 없는 오류입니다.");
/* 기타 오류 메세지 정의 끝*/
private final ErrorResponse errorResponse;
public String getMessage() {
return this.errorResponse.getMessage();
}
public String getErrCode() {
return this.errorResponse.getErrCode();
}
public HttpStatus getHttpStatus() {
return this.errorResponse.getHttpStatus();
}
ErrorEnum(HttpStatus httpStatus, String errCode, String message) {
this.errorResponse = new ErrorResponse(httpStatus, errCode, message);
}
@Getter
public static class ErrorResponse {
private final HttpStatus httpStatus;
private final String errCode;
private final String message;
public ErrorResponse(HttpStatus httpStatus, String errCode, String message) {
this.httpStatus = httpStatus;
this.errCode = errCode;
this.message = message;
}
}
}

View File

@@ -0,0 +1,17 @@
package com.auth.userserver.common.exception;
import com.auth.userserver.common.constants.ErrorEnum;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
public class CustomException extends RuntimeException{
protected HttpStatus status;
protected ErrorEnum errorEnum;
public CustomException(ErrorEnum errorEnum) {
super(errorEnum.toString());
this.status = errorEnum.getErrorResponse().getHttpStatus();
this.errorEnum = errorEnum;
}
}

View File

@@ -0,0 +1,31 @@
package com.auth.userserver.common.exception;
import com.auth.userserver.common.constants.ErrorEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({ CustomException.class })
public void handleCustomException(HttpServletRequest request, HttpServletResponse response, CustomException ex) throws IOException {
ErrorEnum errorEnum = ex.getErrorEnum();
response.sendError(ex.getStatus().value(), errorEnum.getMessage());
log.error("{} 시스템 오류 감지 : {}", errorEnum.getErrCode(), errorEnum.getMessage(), ex);
}
@ExceptionHandler({ Exception.class })
public void handleException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException {
ex.printStackTrace();
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "서버 내부 오류입니다.");
logger.error("알 수 없는 오류 감지.", ex);
}
}

View File

@@ -0,0 +1,16 @@
package com.auth.userserver.common.util;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class URLCombiner {
@Value("${server.host}")
private String host;
@Value("${server.port}")
private String port;
public String combinePathWithHost(String path){
return "http://"+host+":"+port+path;
}
}

View File

@@ -0,0 +1,42 @@
package com.auth.userserver.controller;
import com.auth.userserver.dto.SignUpRequest;
import com.auth.userserver.service.SignUpService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.Objects;
import java.util.UUID;
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/api/signup")
@RestController
public class SignUpController {
private final SignUpService signUpService;
@PostMapping
ResponseEntity<?> signUp(@RequestBody @Valid SignUpRequest signUpRequest, Errors error){
if(error.hasErrors()) {
String errMsg = Objects.requireNonNull(error.getFieldError()).getDefaultMessage();
log.info(errMsg);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errMsg);
}
signUpService.signUp(signUpRequest);
return ResponseEntity.ok().build();
}
@GetMapping("/confirm")
ResponseEntity<?> verifyEmail(@RequestParam(required = true) UUID confirmToken){
boolean isVerified = signUpService.verfiyEmail(confirmToken);
if(!isVerified){
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok().build();
}
}

View File

@@ -0,0 +1,38 @@
package com.auth.userserver.controller;
import com.auth.userserver.dto.PageLimitRequest;
import com.auth.userserver.dto.UserInfoResponse;
import com.auth.userserver.model.Users;
import com.auth.userserver.service.UserInfoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.Objects;
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/api/user")
@RestController
public class UserController {
private final UserInfoService userInfoService;
@GetMapping(produces = "application/json; charset=utf-8")
public ResponseEntity<?> getUsersInfo(@Valid PageLimitRequest pageLimitRequest, Errors error){
if(error.hasErrors()) {
String errMsg = Objects.requireNonNull(error.getFieldError()).getDefaultMessage();
log.info(errMsg);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errMsg);
}
Page<Users> userList = userInfoService.getUsersInfo(pageLimitRequest.of());
Page<UserInfoResponse> userInfoResponses = userList.map(UserInfoResponse::of);
return ResponseEntity.ok(userInfoResponses);
}
}

View File

@@ -0,0 +1,28 @@
package com.auth.userserver.dto;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.domain.Sort;
import javax.validation.constraints.Max;
@Setter
@Getter
public class PageLimitRequest {
private int page;
@Max(value = 100, message = "요청할 수 있는 size의 최대크기는 100입니다.")
private int size;
private Sort.Direction sortDirection;
private String sortColumn;
public void setPage(int page) {
this.page = page <= 0 ? 1 : page;
}
public org.springframework.data.domain.PageRequest of() {
return org.springframework.data.domain.PageRequest.of(page -1, size, sortDirection, sortColumn);
}
}

View File

@@ -0,0 +1,29 @@
package com.auth.userserver.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@AllArgsConstructor
@RequiredArgsConstructor
@Setter @Getter
public class SignUpRequest {
@NotBlank(message = "이름은 필수 입력 값입니다.")
@Length(min = 1, max = 30, message = "이름의 길이가 1부터 30 사이가 아닙니다.")
private String name;
@NotBlank(message = "이메일은 필수 입력 값입니다.")
@Email(message = "이메일 형식에 맞지 않습니다.")
private String email;
@NotBlank(message = "비밀번호은 필수 입력 값입니다.")
@Pattern(regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}",
message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 20자의 비밀번호여야 합니다.")
private String password;
}

View File

@@ -0,0 +1,33 @@
package com.auth.userserver.dto;
import com.auth.userserver.model.Users;
import lombok.*;
import java.time.LocalDateTime;
import java.util.UUID;
@AllArgsConstructor
@RequiredArgsConstructor
@Getter
@Setter
@Builder
public class UserInfoResponse {
private UUID id;
private String name;
private String email;
private LocalDateTime createDt;
private LocalDateTime updateDt;
public static UserInfoResponse of(Users users){
return UserInfoResponse.builder()
.createDt(users.getCreateDt())
.name(users.getName())
.email(users.getEmail())
.updateDt(users.getUpdateDt())
.id(users.getId()).build();
}
}

View File

@@ -0,0 +1,38 @@
package com.auth.userserver.model;
import jdk.jfr.Registered;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Type;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
@AllArgsConstructor
@RequiredArgsConstructor
@Getter
@Builder
@Entity
public class EmailConfirmToken {
@Type(type="uuid-char")
@GeneratedValue(generator = "UUID")
@GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator"
)
@Id
private UUID id;
@CreationTimestamp
@Column(name = "create_dt", updatable = false)
private LocalDateTime createDt;
@OneToOne(targetEntity = Users.class, fetch = FetchType.EAGER)
@JoinColumn(name = "users_id", nullable = false)
private Users users;
}

View File

@@ -0,0 +1,75 @@
package com.auth.userserver.model;
import com.auth.userserver.dto.SignUpRequest;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
@AllArgsConstructor
@RequiredArgsConstructor
@Getter
@Setter
@Builder
@Entity
public class Users {
@Type(type="uuid-char")
@GeneratedValue(generator = "UUID")
@GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator"
)
@Id
private UUID id;
@Column(name = "name",length = 30)
private String name;
@Column(unique = true)
private String email;
private String password;
@Column(name = "email_verified")
private boolean emailVerified;
@Column(name = "user_type", nullable = false)
private int userType;
@CreationTimestamp
@Column(name = "create_dt", updatable = false)
private LocalDateTime createDt;
@Setter
@UpdateTimestamp
@Column(name = "update_dt")
private LocalDateTime updateDt;
public static Users of(SignUpRequest signUpRequest){
return Users.builder()
.name(signUpRequest.getName())
.email(signUpRequest.getEmail())
.password(signUpRequest.getPassword())
.emailVerified(false)
.userType(1).build();
}
public static Users of(UUID id, SignUpRequest signUpRequest){
return Users.builder()
.id(id)
.name(signUpRequest.getName())
.email(signUpRequest.getEmail())
.password(signUpRequest.getPassword())
.emailVerified(false)
.userType(1).build();
}
public boolean isEmailVerified(){
return emailVerified;
}
}

View File

@@ -0,0 +1,11 @@
package com.auth.userserver.repository;
import com.auth.userserver.model.EmailConfirmToken;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface EmailConfirmTokenRepository extends JpaRepository<EmailConfirmToken, Integer> {
Optional<EmailConfirmToken> findById(UUID id);
}

View File

@@ -0,0 +1,14 @@
package com.auth.userserver.repository;
import com.auth.userserver.model.Users;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<Users, Integer> {
Page<Users> findAllByEmailVerifiedTrue(Pageable pageable);
Optional<Users> findByEmail(String email);
}

View File

@@ -0,0 +1,51 @@
package com.auth.userserver.service;
import com.auth.userserver.common.constants.ErrorEnum;
import com.auth.userserver.common.exception.CustomException;
import com.auth.userserver.common.util.URLCombiner;
import com.auth.userserver.model.EmailConfirmToken;
import com.auth.userserver.model.Users;
import com.auth.userserver.repository.EmailConfirmTokenRepository;
import com.auth.userserver.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class EmailService {
private final JavaMailSender mailSender;
private final URLCombiner urlCombiner;
private final EmailConfirmTokenRepository emailConfirmTokenRepository;
private final UserRepository userRepository;
@Value("${spring.mail.username}")
private String senderEmail;
public void sendSignUpEmail(Users users) {
Optional<Users> savedUser = userRepository.findByEmail(users.getEmail());
if(savedUser.isEmpty())
throw new CustomException(ErrorEnum.USER_NOT_FOUND);
if(savedUser.get().isEmailVerified())
throw new CustomException(ErrorEnum.USER_ALREADY_CONFIRMED);
EmailConfirmToken emailConfirmToken = EmailConfirmToken.builder()
.users(users).build();
emailConfirmTokenRepository.save(emailConfirmToken);
String path = UriComponentsBuilder.fromPath("/api/signup/confirm")
.queryParam("confirmToken", emailConfirmToken.getId()).toUriString();
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(emailConfirmToken.getUsers().getEmail());
message.setFrom(senderEmail);
message.setSubject("[소울메이츠] 회원 가입 인증 이메일");
message.setText("회원 인증 주소: " + urlCombiner.combinePathWithHost(path));
mailSender.send(message);
}
}

View File

@@ -0,0 +1,56 @@
package com.auth.userserver.service;
import com.auth.userserver.common.constants.ErrorEnum;
import com.auth.userserver.common.exception.CustomException;
import com.auth.userserver.dto.SignUpRequest;
import com.auth.userserver.model.EmailConfirmToken;
import com.auth.userserver.model.Users;
import com.auth.userserver.repository.EmailConfirmTokenRepository;
import com.auth.userserver.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@RequiredArgsConstructor
@Service
public class SignUpService {
private final UserRepository userRepository;
private final EmailConfirmTokenRepository emailConfirmTokenRepository;
private final EmailService emailService;
private final Pbkdf2PasswordEncoder passwordEncoder;
@Transactional
public void signUp(SignUpRequest signUpRequest){
signUpRequest.setPassword(passwordEncoder.encode(signUpRequest.getPassword()));
Optional<Users> savedUser = userRepository.findByEmail(signUpRequest.getEmail());
if(savedUser.isPresent()){
if(savedUser.get().isEmailVerified())
throw new CustomException(ErrorEnum.USER_ALREADY_EXIST);
Users users = userRepository.save(Users.of(savedUser.get().getId(), signUpRequest));
emailService.sendSignUpEmail(users);
return;
}
Users users = userRepository.save(Users.of(signUpRequest));
emailService.sendSignUpEmail(users);
}
public boolean verfiyEmail(UUID confirmTokenId){
Optional<EmailConfirmToken> confirmToken = emailConfirmTokenRepository.findById(confirmTokenId);
if(confirmToken.isEmpty()){
log.info("token is empty [{}]",confirmTokenId);
return false;
}
Users user = userRepository.findByEmail(confirmToken.get().getUsers().getEmail()).get();
user.setEmailVerified(true);
userRepository.save(user);
return true;
}
}

View File

@@ -0,0 +1,18 @@
package com.auth.userserver.service;
import com.auth.userserver.model.Users;
import com.auth.userserver.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class UserInfoService {
private final UserRepository userRepository;
public Page<Users> getUsersInfo(Pageable pageable){
return userRepository.findAllByEmailVerifiedTrue(pageable);
}
}