Skip to content

Commit

Permalink
Merge pull request #6 from drinkParty/feat/#3-user-login
Browse files Browse the repository at this point in the history
[feat-3] 회원가입, 로그인 API
  • Loading branch information
chaerlo127 committed Dec 9, 2023
2 parents a98bc93 + 4bdbc89 commit 1a64a22
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.s1350.sooljangmacha.global.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.s1350.sooljangmacha.global.exception.BaseResponseCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -9,6 +10,7 @@
@Getter
@RequiredArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BaseResponse<T> {
private final int status;
private final String code;
Expand All @@ -30,7 +32,7 @@ public static <T> BaseResponse<T> OK(@Nullable T data) {
}

public static BaseResponse error(BaseResponseCode baseResponseCode, String message) {
return new BaseResponse<>(baseResponseCode.getStatus().value(), baseResponseCode.getCode(), baseResponseCode.getMessage());
return new BaseResponse<>(baseResponseCode.getStatus().value(), baseResponseCode.getCode(), message);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public enum BaseResponseCode {
DATABASE_ERROR("E0003", HttpStatus.INTERNAL_SERVER_ERROR, "데이터베이스 관련 에러입니다."),
INTERNAL_SERVER_ERROR("E0004", HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러입니다."),

USER_NOT_FOUND("U0001", HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다.");
USER_NOT_FOUND("U0001", HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다."),
USER_ALREADY_EXIST("U0002", HttpStatus.BAD_REQUEST, "이미 가입된 유저입니다.");

public final String code;
public final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.s1350.sooljangmacha.global.resolver;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = {EnumValidator.class})
public @interface EnumValid {
String message() default "잘못된 값입니다.";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

Class<? extends java.lang.Enum<?>> enumClass();

boolean ignoreCase() default false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.s1350.sooljangmacha.global.resolver;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class EnumValidator implements ConstraintValidator<EnumValid, String> {

private EnumValid annotation;

@Override
public void initialize(EnumValid constraintAnnotation) {
this.annotation = constraintAnnotation;
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Object[] enumValues = this.annotation.enumClass().getEnumConstants();

if (enumValues != null) {
for (Object enumValue : enumValues) {
if (value.equals(enumValue.toString()) || (this.annotation.ignoreCase() && value.equalsIgnoreCase(enumValue.toString()))) {
return true;
}
}
}
return false;
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/s1350/sooljangmacha/global/utils/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;

import static com.s1350.sooljangmacha.global.Constants.BEARER_PREFIX;
import static com.s1350.sooljangmacha.global.Constants.CLAIM_NAME;
Expand All @@ -19,6 +20,8 @@ public class JwtUtil {
@Value("${jwt.secret}")
private String jwtSecret;

private final long accessTokenExpiryTime = 1000L * 60 * 60 * 24 * 14; // 2주

public static String replaceBearer(String header) {
return header.substring(BEARER_PREFIX.length());
}
Expand Down Expand Up @@ -57,4 +60,25 @@ private Claims getBody(String token) {
return e.getClaims();
}
}

public String issuedAccessToken(Long userId) {
return issuedToken("access_token", accessTokenExpiryTime, String.valueOf(userId));
}

public String issuedToken(String tokenName, long expiryTime, String userId) {
final Date now = new Date();

final Claims claims = Jwts.claims()
.setSubject(tokenName)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + expiryTime));

claims.put(CLAIM_NAME, userId);

return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setClaims(claims)
.signWith(getSigningKey())
.compact();
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
package com.s1350.sooljangmacha.user.controller;

import com.s1350.sooljangmacha.global.dto.BaseResponse;
import com.s1350.sooljangmacha.user.dto.request.LoginReq;
import com.s1350.sooljangmacha.user.dto.request.SignupReq;
import com.s1350.sooljangmacha.user.dto.response.LoginRes;
import com.s1350.sooljangmacha.user.service.UserService;
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 org.springframework.web.bind.annotation.*;
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 javax.validation.Valid;

@Tag(name = "users", description = "유저 API")
@RestController
Expand All @@ -13,8 +26,27 @@
public class UserController {
private final UserService userService;

// @Operation(summary = "로그인", description = "")
// @PostMapping("/login")
@Operation(summary = "회원가입", description = "회원가입을 한다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "(S0001)요청에 성공했습니다."),
@ApiResponse(responseCode = "400", description = "(E0001)잘못된 요청입니다. <br> (E0001)닉네임을 입력해 주세요. <br> (E0001)이메일을 입력해 주세요. <br> (E0001)provider를 입력해 주세요. <br> (E0001)올바른 이메일 형식으로 입력해 주세요. <br> (E0001)잘못된 provider 값 입니다. <br> (U0002)이미 가입된 유저입니다.", content = @Content(schema = @Schema(implementation = BaseResponse.class)))
})
@PostMapping("/signup")
public BaseResponse<LoginRes> signup(@RequestBody @Valid SignupReq signupReq) {
return BaseResponse.OK(userService.signup(signupReq));
}

@Operation(summary = "로그인", description = "로그인을 한다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "(S0001)요청에 성공했습니다."),
@ApiResponse(responseCode = "400", description = "(E0001)잘못된 요청입니다. <br> (E0001)이메일을 입력해 주세요. <br> (E0001)provider를 입력해 주세요. <br> (E0001)올바른 이메일 형식으로 입력해 주세요. <br> (E0001)잘못된 provider 값 입니다. <br>", content = @Content(schema = @Schema(implementation = BaseResponse.class))),
@ApiResponse(responseCode = "404", description = "(U0001)존재하지 않는 유저입니다.", content = @Content(schema = @Schema(implementation = BaseResponse.class))),
})
@PostMapping("/login")
public BaseResponse<LoginRes> login(@RequestBody @Valid LoginReq loginReq) {
return BaseResponse.OK(userService.login(loginReq));
}


// @Operation(summary = "로그아웃", description = "")
// @PostMapping("/logout")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.s1350.sooljangmacha.user.dto.request;

import com.s1350.sooljangmacha.global.resolver.EnumValid;
import com.s1350.sooljangmacha.user.entity.Provider;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;

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

@Data
@NoArgsConstructor
public class LoginReq {

@Schema(type = "String", description = "유저 이메일", example = "ex@naver.com", required = true)
@NotBlank(message = "이메일을 입력해 주세요.")
@Email(message = "올바른 이메일 형식으로 입력해 주세요.")
private String email;

@Schema(type = "String", description = "유저 로그인 종류", example = "KAKAO", allowableValues = {"KAKAO", "APPLE"}, required = true)
@NotBlank(message = "provider를 입력해 주세요.")
@EnumValid(enumClass = Provider.class, ignoreCase = true, message = "잘못된 provider 값 입니다.")
private String provider;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.s1350.sooljangmacha.user.dto.request;

import com.s1350.sooljangmacha.global.resolver.EnumValid;
import com.s1350.sooljangmacha.user.entity.Provider;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;

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

@Data
@NoArgsConstructor
public class SignupReq {

@Schema(type = "String", description = "유저 닉네임", example = "띵동", required = true)
@NotBlank(message = "닉네임을 입력해 주세요.")
private String nickname;

@Schema(type = "String", description = "유저 이미지 key", example = "ex.png")
private String imgKey;

@Schema(type = "String", description = "유저 주소", example = "서울시 서대문구")
private String address;

@Schema(type = "String", description = "유저 이메일", example = "ex@naver.com", required = true)
@NotBlank(message = "이메일을 입력해 주세요.")
@Email(message = "올바른 이메일 형식으로 입력해 주세요.")
private String email;

@Schema(type = "String", description = "유저 로그인 종류", example = "KAKAO", allowableValues = {"KAKAO", "APPLE"}, required = true)
@NotBlank(message = "provider를 입력해 주세요.")
@EnumValid(enumClass = Provider.class, ignoreCase = true, message = "잘못된 provider 값 입니다.")
private String provider;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.s1350.sooljangmacha.user.dto.response;

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

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginRes {

@Schema(type = "String", description = "액세스 토큰")
private String accessToken;

public static LoginRes toEntity(String accessToken) {
return LoginRes.builder()
.accessToken(accessToken)
.build();
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/s1350/sooljangmacha/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.s1350.sooljangmacha.global.entity.BaseEntity;
import com.s1350.sooljangmacha.store.entity.StoreLike;
import com.s1350.sooljangmacha.user.dto.request.SignupReq;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.DynamicInsert;
Expand All @@ -25,6 +27,10 @@ public class User extends BaseEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotNull
@Size(max = 255)
private String email;

@NotNull
@Size(max = 30)
private String nickname;
Expand All @@ -43,4 +49,22 @@ public class User extends BaseEntity {
@OneToMany(mappedBy = "user")
private List<StoreLike> storeLikeList = new ArrayList<>();

public static User toEntity(SignupReq request) {
return User.builder()
.email(request.getEmail())
.nickname(request.getNickname())
.address(request.getAddress())
.imgKey(request.getImgKey())
.provider(Provider.valueOf(request.getProvider()))
.build();
}

@Builder
public User(String email, String nickname, String address, String imgKey, Provider provider) {
this.email = email;
this.nickname = nickname;
this.address = address;
this.imgKey = imgKey;
this.provider = provider;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.s1350.sooljangmacha.user.repository;

import com.s1350.sooljangmacha.user.entity.Provider;
import com.s1350.sooljangmacha.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByIdAndIsEnable(Long id, boolean isEnable);

Optional<User> findByEmailAndProviderAndIsEnable(String email, Provider provider, boolean isEnable);

boolean existsByEmailAndProviderAndIsEnable(String email, Provider provider, boolean isEnable);

}
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
package com.s1350.sooljangmacha.user.service;


import com.s1350.sooljangmacha.global.exception.BaseException;
import com.s1350.sooljangmacha.global.exception.BaseResponseCode;
import com.s1350.sooljangmacha.global.utils.JwtUtil;
import com.s1350.sooljangmacha.user.dto.request.LoginReq;
import com.s1350.sooljangmacha.user.dto.request.SignupReq;
import com.s1350.sooljangmacha.user.dto.response.LoginRes;
import com.s1350.sooljangmacha.user.entity.Provider;
import com.s1350.sooljangmacha.user.entity.User;
import com.s1350.sooljangmacha.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


@Service
@RequiredArgsConstructor
public class UserService {
private UserRepository userRepository;
private final UserRepository userRepository;
private final JwtUtil jwtUtil;

// 로그인
public LoginRes login(LoginReq request) {
User user = userRepository.findByEmailAndProviderAndIsEnable(request.getEmail(), Provider.valueOf(request.getProvider()), true)
.orElseThrow(() -> new BaseException(BaseResponseCode.USER_NOT_FOUND));
return LoginRes.toEntity(jwtUtil.issuedAccessToken(user.getId()));
}

// 회원가입
@Transactional
public LoginRes signup(SignupReq request) {
if (userRepository.existsByEmailAndProviderAndIsEnable(request.getEmail(), Provider.valueOf(request.getProvider()), true))
throw new BaseException(BaseResponseCode.USER_ALREADY_EXIST);
User user = userRepository.save(User.toEntity(request));
return LoginRes.toEntity(jwtUtil.issuedAccessToken(user.getId()));
}

// 로그아웃

Expand Down

0 comments on commit 1a64a22

Please sign in to comment.