Skip to content

Commit

Permalink
Merge pull request #4 from SWYP-4rd-6/feat/user
Browse files Browse the repository at this point in the history
User 기능 추가
  • Loading branch information
wocks1123 authored May 1, 2024
2 parents 97703bd + 05d9b32 commit 0d81d14
Show file tree
Hide file tree
Showing 52 changed files with 1,692 additions and 101 deletions.
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ dependencies {
// validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// security
implementation 'org.springframework.boot:spring-boot-starter-security'

// jwt
compileOnly 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// AWS S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
//payment
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.swygbro.trip.backend.domain.auth.api;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.swygbro.trip.backend.domain.auth.application.LoginService;
import com.swygbro.trip.backend.domain.auth.dto.LoginRequest;
import com.swygbro.trip.backend.domain.auth.dto.OAuth2LoginRequest;
import com.swygbro.trip.backend.global.jwt.dto.TokenDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/login")
@Tag(name = "Auth", description = """
- 로그인 기능을 제공하는 API 입니다.
## 주요 기능
- 로그인
- 구글 소셜 로그인(구현 예정)
""")
public class LoginController {

private static final Logger log = LoggerFactory.getLogger(LoginController.class);
private final LoginService loginService;

@PostMapping
@Operation(summary = "로그인", description = """
# 로그인
사용자의 이메일과 비밀번호를 입력하여 로그인합니다.
## 응답
- 로그인 성공 시 `200` 코드와 함께 토큰을 반환합니다.
- 토큰은 `access_token`과 `refresh_token`으로 구성되어 있습니다.
- 로그인 실패 시 `400` 에러를 반환합니다.
""")
@ApiResponse(
responseCode = "200",
description = "로그인 성공 시 토큰을 반환합니다."
)
public TokenDto login(@Valid @RequestBody LoginRequest dto) {
return loginService.login(dto);
}

@PostMapping(value = "/google")
@Operation(summary = "구글 소셜 로그인", description = """
# 구글 소셜 로그인
구글 소셜 로그인을 통해 로그인합니다.
클라이언트에서 구글 로그인 후 발급받은 code를 인자로 전달하면 소셜 로그인이 진행됩니다.
s
## 응답
- 로그인 성공 시 `200` 코드와 함께 토큰을 반환합니다.
- 토큰은 `access_token`과 `refresh_token`으로 구성되어 있습니다.
- 로그인 실패 시 `400` 에러를 반환합니다.
""")
public TokenDto googleLogin(@RequestBody OAuth2LoginRequest code) throws JsonProcessingException {
return loginService.loginGoogle(code.getCode());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.swygbro.trip.backend.domain.auth.application;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.swygbro.trip.backend.domain.auth.dto.GoogleUserInfo;
import com.swygbro.trip.backend.domain.auth.dto.LoginRequest;
import com.swygbro.trip.backend.domain.auth.exception.LoginFailException;
import com.swygbro.trip.backend.domain.user.domain.User;
import com.swygbro.trip.backend.domain.user.domain.UserRepository;
import com.swygbro.trip.backend.global.jwt.TokenService;
import com.swygbro.trip.backend.global.jwt.dto.TokenDto;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Service
@RequiredArgsConstructor
public class LoginService {

private final UserRepository userRepository;
private final TokenService tokenService;
private final PasswordEncoder passwordEncoder;
private final RestTemplate restTemplate;


@Value("${google.client.id}")
private String clientId;
@Value("${google.client.secret}")
private String secretPassword;
@Value("${google.client.redirect}")
private String redirectUri;

public TokenDto login(LoginRequest dto) {
User user = userRepository.findByEmail(dto.getEmail())
.orElseThrow(LoginFailException::new);

if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
throw new LoginFailException();
}

return tokenService.generateToken(user.getEmail());
}

public TokenDto loginGoogle(String code) throws JsonProcessingException {
String accessToken = getGoogleAccessToken(code);
GoogleUserInfo userInfo = getGoogleUserInfo(accessToken);
// TODO 회원등록 추가
return tokenService.generateToken(userInfo.getEmail());
}

public String getGoogleAccessToken(String code) throws JsonProcessingException {
URI uri = UriComponentsBuilder
.fromUriString("https://oauth2.googleapis.com/token")
.encode()
.build()
.toUri();

HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", clientId);
body.add("client_secret", secretPassword);
body.add("redirect_uri", redirectUri);
body.add("code", code);

RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity.post(uri).headers(headers).body(body);
ResponseEntity<String> response = restTemplate.exchange(requestEntity, String.class);

JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());

return jsonNode.get("access_token").asText();
}

private GoogleUserInfo getGoogleUserInfo(String googleAccessToken) throws JsonProcessingException {
URI uri = UriComponentsBuilder
.fromUriString("https://www.googleapis.com/oauth2/v2/userinfo")
.queryParam("access_token", googleAccessToken)
.encode()
.build()
.toUri();

ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
return new ObjectMapper().readValue(responseEntity.getBody(), GoogleUserInfo.class);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.swygbro.trip.backend.domain.auth.dto;

import lombok.Getter;

@Getter
public class GoogleUserInfo {
private String id;
private String email;
private boolean verified_email;
private String picture;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.swygbro.trip.backend.domain.auth.dto;

import com.swygbro.trip.backend.global.dto.RequestDto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class LoginRequest extends RequestDto {
@NotBlank
@Email
@Schema(description = "사용자 이메일", example = "[email protected]")
private final String email;

@NotBlank
@Size(min = 8, max = 20)
@Schema(description = "사용자 비밀번호", example = "password123!")
private final String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.swygbro.trip.backend.domain.auth.dto;

import lombok.Getter;

@Getter
public class OAuth2LoginRequest {
private String code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.swygbro.trip.backend.domain.auth.exception;

import com.swygbro.trip.backend.global.exception.BaseException;
import org.springframework.http.HttpStatus;

public class LoginFailException extends BaseException {

public LoginFailException() {
super(HttpStatus.BAD_REQUEST, "로그인에 실패했습니다.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class GuideProductService {
// 가이드 상품 생성
@Transactional
public GuideProductDto createGuideProduct(GuideProductRequest request, List<MultipartFile> images) {
User user = getUser(request.getAccount());
User user = getUser(request.getEmail());

isValidLocation(request.getLongitude(), request.getLatitude());
GuideProduct product = new GuideProduct(user, request.getTitle(), request.getDescription(),
Expand Down Expand Up @@ -62,7 +62,7 @@ public GuideProductDto getProduct(Long productId) {
@Transactional
public GuideProductDto modifyGuideProduct(Long productId, GuideProductRequest edits) {
GuideProduct product = guideProductRepository.findById(productId).orElseThrow(() -> new GuideProductNotFoundException(productId));
User user = getUser(edits.getAccount());
User user = getUser(edits.getEmail());

if (product.getUser() != user) throw new MismatchUserFromCreatorException();
product.setGuideProduct(edits);
Expand All @@ -79,8 +79,8 @@ public void deleteGuideProduct(Long productId) {
}

// 가이드 상품 생성 시 필요한 호스트 정보 불러오가
private User getUser(String account) {
return userRepository.findByAccount(account).orElseThrow(() -> new UserNotFoundException(account));
private User getUser(String email) {
return userRepository.findByEmail(email).orElseThrow(() -> new UserNotFoundException(email));
}

// 가이드 위치 유효한지 검사
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
public class GuideProductDto {
@Schema(description = "상품 ID", example = "1")
private Long id;
private String account;
private String email;
@Schema(description = "상품 제목", example = "신나는 서울 투어")
private String title;
@Schema(description = "상품 설명", example = "서울 *** 여행 가이드 합니다.")
Expand Down Expand Up @@ -46,7 +46,7 @@ public static GuideProductDto fromEntity(GuideProduct product) {

return new GuideProductDto(
product.getId(),
product.getUser().getAccount(),
product.getUser().getEmail(),
product.getTitle(),
product.getDescription(),
product.getPrice(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@AllArgsConstructor
@NoArgsConstructor
public class GuideProductRequest {
private String account;
private String email;
@NotBlank
@Schema(description = "상품 제목", example = "신나는 서울 투어")
private String title;
Expand Down
Loading

0 comments on commit 0d81d14

Please sign in to comment.