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

refactor: 로그인, 회원가입 API 분리 완료 #34

Merged
merged 6 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies {
implementation 'com.google.code.gson:gson:2.10.1' // JSON 라이브러리
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' // Swagger
implementation 'commons-io:commons-io:2.16.1' // Apache Commons IO
implementation 'org.apache.commons:commons-lang3:3.17.0' // Apache Commons Lang
implementation 'com.google.guava:guava:33.3.0-jre' // Google Core Libraries For Java

// Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.stempo.api.domain.application.exception;

public class UserAlreadyExistsException extends RuntimeException {

public UserAlreadyExistsException(String message) {
super(message);
}

public UserAlreadyExistsException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.stempo.api.domain.application.service;

import com.stempo.api.domain.presentation.dto.request.LoginRequestDto;
import com.stempo.api.domain.presentation.dto.response.TokenInfo;
import jakarta.servlet.http.HttpServletRequest;

public interface LoginService {

TokenInfo loginOrRegister(String deviceTag, String password);
TokenInfo login(LoginRequestDto requestDto);

TokenInfo reissueToken(HttpServletRequest request);
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
package com.stempo.api.domain.application.service;

import com.stempo.api.domain.domain.model.User;
import com.stempo.api.domain.presentation.dto.request.LoginRequestDto;
import com.stempo.api.domain.presentation.dto.response.TokenInfo;
import com.stempo.api.global.auth.exception.TokenForgeryException;
import com.stempo.api.global.auth.jwt.JwtTokenProvider;
import com.stempo.api.global.config.CustomAuthenticationProvider;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class LoginServiceImpl implements LoginService {

private final UserService userService;
private final JwtTokenProvider jwtTokenProvider;
private final CustomAuthenticationProvider authenticationManager;

@Override
public TokenInfo loginOrRegister(String deviceTag, String password) {
User user = userService.findById(deviceTag)
.orElseGet(() -> userService.registerUser(deviceTag, password));
return generateToken(user);
public TokenInfo login(LoginRequestDto requestDto) {
String deviceTag = requestDto.getDeviceTag();
String password = requestDto.getPassword();

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(deviceTag, password);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
return jwtTokenProvider.generateToken(authentication);
}

@Override
Expand All @@ -30,10 +39,6 @@ public TokenInfo reissueToken(HttpServletRequest request) {
return reissueToken(refreshToken);
}

private TokenInfo generateToken(User loginUser) {
return jwtTokenProvider.generateToken(loginUser.getDeviceTag(), loginUser.getRole());
}

private void validateRefreshToken(String refreshToken) {
if (!jwtTokenProvider.isRefreshToken(refreshToken)) {
throw new TokenForgeryException("Invalid refresh token.");
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package com.stempo.api.domain.application.service;

import com.stempo.api.domain.domain.model.User;
import com.stempo.api.domain.presentation.dto.request.UserRequestDto;

import java.util.Optional;

public interface UserService {

User registerUser(String deviceTag, String password);
String registerUser(UserRequestDto requestDto);

Optional<User> findById(String id);

boolean existsById(String id);

String getCurrentDeviceTag();

User getCurrentUser();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.stempo.api.domain.application.service;

import com.stempo.api.domain.application.exception.UserAlreadyExistsException;
import com.stempo.api.domain.domain.model.User;
import com.stempo.api.domain.domain.repository.UserRepository;
import com.stempo.api.domain.presentation.dto.request.UserRequestDto;
import com.stempo.api.global.auth.util.AuthUtil;
import com.stempo.api.global.util.PasswordUtil;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Optional;
Expand All @@ -14,28 +17,28 @@
public class UserServiceImpl implements UserService {

private final UserRepository repository;
private final PasswordService passwordService;
private final PasswordUtil passwordUtil;
private final PasswordEncoder passwordEncoder;

@Override
public User registerUser(String deviceTag, String password) {
String rawPassword = password != null ? password : passwordUtil.generateStrongPassword();
User user = User.create(deviceTag, rawPassword);
String encodedPassword = passwordService.encodePassword(user.getPassword());
user.updatePassword(encodedPassword);
return repository.save(user);
public String registerUser(UserRequestDto requestDto) {
String deviceTag = requestDto.getDeviceTag();
String password = requestDto.getPassword();

if (repository.existsById(deviceTag)) {
throw new UserAlreadyExistsException("User already exists.");
}

String finalPassword = StringUtils.isEmpty(password) ? null : passwordEncoder.encode(password);
User user = User.create(deviceTag, finalPassword);
return repository.save(user).getDeviceTag();
}


@Override
public Optional<User> findById(String id) {
return repository.findById(id);
}

@Override
public boolean existsById(String id) {
return repository.existsById(id);
}

@Override
public String getCurrentDeviceTag() {
return AuthUtil.getAuthenticationInfoDeviceTag();
Expand Down
17 changes: 14 additions & 3 deletions src/main/java/com/stempo/api/domain/domain/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@Getter
@Setter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class User {
public class User implements UserDetails {

private String deviceTag;
private String password;
Expand All @@ -22,8 +27,14 @@ public static User create(String deviceTag, String password) {
return new User(deviceTag, password, Role.USER);
}

public void updatePassword(String encodedPassword) {
setPassword(encodedPassword);
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> role.name());
}

@Override
public String getUsername() {
return deviceTag;
}

public boolean isAdmin() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package com.stempo.api.domain.presentation;

import com.stempo.api.domain.application.service.LoginService;
import com.stempo.api.domain.presentation.dto.request.LoginRequestDto;
import com.stempo.api.domain.presentation.dto.response.TokenInfo;
import com.stempo.api.global.common.dto.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
Expand All @@ -23,10 +25,9 @@ public class LoginController {
"일반 계정일 경우 Device-Tag만 기입하면 됨")
@PostMapping("/api/v1/login")
public ApiResponse<TokenInfo> login(
@RequestHeader(value = "Device-Tag", defaultValue = "490154203237518") String deviceTag,
@RequestHeader(value = "Password", required = false) String password
) {
TokenInfo token = loginService.loginOrRegister(deviceTag, password);
@Valid @RequestBody LoginRequestDto requestDto
) {
TokenInfo token = loginService.login(requestDto);
return ApiResponse.success(token);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.stempo.api.domain.presentation;

import com.stempo.api.domain.application.service.UserService;
import com.stempo.api.domain.presentation.dto.request.UserRequestDto;
import com.stempo.api.global.common.dto.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@Tag(name = "User", description = "회원")
public class UserController {

private final UserService userService;

@Operation(summary = "회원 가입", description = "ROLE_ANONYMOUS 이상의 권한이 필요함")
@PostMapping("/api/v1/users")
public ApiResponse<String> registerUser(
@Valid @RequestBody UserRequestDto requestDto
) {
String deviceTag = userService.registerUser(requestDto);
return ApiResponse.success(deviceTag);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.stempo.api.domain.presentation.dto.request;

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

@Getter
@Setter
public class LoginRequestDto {

@NotBlank(message = "deviceTag is required")
@Schema(description = "디바이스 식별자" , example = "490154203237518", requiredMode = Schema.RequiredMode.REQUIRED)
private String deviceTag;

@Schema(description = "비밀번호" , example = "password")
private String password;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ public class RhythmRequestDto {

@NotNull(message = "BPM is required")
@Range(min = 10, max = 200, message = "BPM must be between 10 and 200")
@Schema(description = "BPM", example = "60", minimum = "10", maximum = "200")
@Schema(description = "BPM", example = "60", minimum = "10", maximum = "200", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer bpm;

@NotNull(message = "Bit is required")
@Range(min = 1, max = 8, message = "Bit must be between 1 and 8")
@Schema(description = "Bit", example = "4", minimum = "1", maximum = "8")
@Schema(description = "Bit", example = "4", minimum = "1", maximum = "8", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer bit;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.stempo.api.domain.presentation.dto.request;

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

@Getter
@Setter
public class UserRequestDto {

@NotBlank(message = "deviceTag is required")
@Schema(description = "디바이스 식별자" , example = "490154203237518", requiredMode = Schema.RequiredMode.REQUIRED)
private String deviceTag;

@Schema(description = "비밀번호" , example = "password")
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.stempo.api.global.auth.application;

import com.stempo.api.domain.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findById(username)
.map(user -> (UserDetails) user)
.orElseThrow(() -> new UsernameNotFoundException("[User] id: " + username + " not found"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
chain.doFilter(request, response);
}

private boolean authenticateToken(HttpServletRequest request) throws IOException {
private boolean authenticateToken(HttpServletRequest request) {
String token = jwtTokenProvider.resolveToken(request);
if (Objects.nonNull(token) && jwtTokenProvider.validateTokenSilently(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
Expand Down
Loading