Skip to content

Commit

Permalink
[BE] OAuth 회원가입 (#149)
Browse files Browse the repository at this point in the history
* docs: oauth 회원가입을 위한 컬럼 추가

* feat: OAuth 최초 로그인시 추가 회원 정보를 받도록 기능 구현

* chore : github action deploy.yml 수정

---------

Co-authored-by: pie <[email protected]>
  • Loading branch information
23Yong and pie2457 committed Aug 16, 2023
1 parent 8540f56 commit 4e6a32c
Show file tree
Hide file tree
Showing 13 changed files with 125 additions and 23 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ name: ci
on:
pull_request:
branches:
- be-w3
- fe-w3
- be-w4
- fe-w4

env:
AWS_REGION: ap-northeast-2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import kr.codesquad.issuetracker.domain.UserAccount;
import kr.codesquad.issuetracker.exception.ApplicationException;
import kr.codesquad.issuetracker.exception.ErrorCode;
import kr.codesquad.issuetracker.exception.InitialLoginException;
import kr.codesquad.issuetracker.infrastructure.persistence.UserAccountRepository;
import kr.codesquad.issuetracker.infrastructure.security.hash.PasswordEncoder;
import kr.codesquad.issuetracker.infrastructure.security.jwt.JwtProvider;
Expand Down Expand Up @@ -52,24 +53,33 @@ public LoginSuccessResponse login(final String loginId, final String password) {
return new LoginSuccessResponse(token, findUserAccount.getProfileUrl(), findUserAccount.getLoginId());
}

@Transactional
@Transactional(readOnly = true)
public LoginSuccessResponse oauthLogin(final String code) {
GithubUser oAuthUser = githubClient.getOAuthUser(code);
String username = oAuthUser.getUsername();
String email = oAuthUser.getEmail();

UserAccount userAccount = userAccountRepository.findByLoginId(username)
.orElseGet(() -> new UserAccount(username, "", oAuthUser.getAvatarUrl()));
Integer userId = userAccount.getId();
if (userAccount.getId() == null) {
userId = userAccountRepository.save(userAccount);
}
UserAccount userAccount = userAccountRepository.findByEmail(email) // 최초로그인의 경우 email 응답
.orElseThrow(() -> new InitialLoginException("최초 로그인입니다.", email));

LoginSuccessResponse.TokenResponse token = jwtProvider.createToken(Map.of(
"userId", String.valueOf(userAccount.getId()),
"loginId", userAccount.getLoginId()
));

return new LoginSuccessResponse(token, userAccount.getProfileUrl(), userAccount.getLoginId());
}

@Transactional
public LoginSuccessResponse oauthSignup(String email, String username) {
UserAccount userAccount = UserAccount.fromOAuthData(username, email);
int id = userAccountRepository.save(userAccount);

LoginSuccessResponse.TokenResponse token = jwtProvider.createToken(Map.of(
"userId", String.valueOf(userId),
"loginId", username
"userId", String.valueOf(id),
"loginId", userAccount.getLoginId()
));

return new LoginSuccessResponse(token, userAccount.getProfileUrl(), username);
return new LoginSuccessResponse(token, userAccount.getProfileUrl(), userAccount.getLoginId());
}

public String getOAuthLoginPageUrl() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ public GithubUser(Map<String, Object> userInfo) {
this.userInfo = userInfo;
}

public String getUsername() {
String login = userInfo.get("login").toString();
String id = userInfo.get("id").toString();
return login + id;
public String getEmail() {
return userInfo.get("email").toString();
}

public String getAvatarUrl() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class UserAccount {
private Integer id;
private String loginId;
private String password;
private String email;
private String profileUrl;
private Boolean isDeleted;

Expand All @@ -31,11 +32,15 @@ public UserAccount(String loginId, String password, String avatarUrl) {
this(null, loginId, password, avatarUrl);
}

public static UserAccount fromOAuthData(String username, String email) {
return new UserAccount(null, username, "", email, DEFAULT_PROFILE_URL, false);
}

public boolean isSamePassword(String password) {
return this.password.equals(password);
}

public static UserAccount createUserProfile(Integer id, String loginId, String profileUrl) {
return new UserAccount(id, loginId, null, profileUrl, false);
return new UserAccount(id, loginId, null, null, profileUrl, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ public enum ErrorCode {
MILESTONE_NOT_FOUND(404, "존재하지 않는 마일스톤입니다."),

// OAUTH
GITHUB_FAILED_LOGIN(500, "소셜 로그인에 실패했습니다.");
GITHUB_FAILED_LOGIN(500, "소셜 로그인에 실패했습니다."),
GITHUB_FAILED_GET_EMAIL(500, "Github email 정보를 가져오는데 실패했습니다."),
INITIAL_LOGIN(205, "최초 로그인이기 때문에 username 정보가 필요합니다.");

private final int statusCode;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package kr.codesquad.issuetracker.exception;

import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
Expand Down Expand Up @@ -29,4 +31,10 @@ public ResponseEntity<ErrorResponse> handleApplicationException(OAuthAccessToken
return ResponseEntity.status(errorCode.getStatusCode())
.body(new ErrorResponse(errorCode, e.getMessage()));
}

@ExceptionHandler(InitialLoginException.class)
public ResponseEntity<Map<String, String>> handleInitialLoginException(InitialLoginException e) {
return ResponseEntity.status(HttpStatus.ACCEPTED)
.body(Map.of("email", e.getEmail()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package kr.codesquad.issuetracker.exception;

import lombok.Getter;

@Getter
public class InitialLoginException extends RuntimeException {

private final String email;

public InitialLoginException(String message, String email) {
super(message);
this.email = email;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import org.springframework.dao.support.DataAccessUtils;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
Expand All @@ -24,11 +25,12 @@ public UserAccountRepository(DataSource dataSource) {
this.jdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
this.jdbcInsert = new SimpleJdbcInsert(dataSource)
.withTableName("user_account")
.usingColumns("login_id", "password", "profile_url")
.usingColumns("login_id", "password", "profile_url", "email")
.usingGeneratedKeyColumns("id");
}

public Integer save(UserAccount userAccount) {
public int save(UserAccount userAccount) {
// return user_account PK
return jdbcInsert.executeAndReturnKey(new BeanPropertySqlParameterSource(userAccount)).intValue();
}

Expand All @@ -39,7 +41,9 @@ public Boolean existsByLoginId(String loginId) {

public Optional<UserAccount> findByLoginId(String loginId) {
return Optional.ofNullable(DataAccessUtils.singleResult(jdbcTemplate.query(
"SELECT id, login_id, password, profile_url FROM user_account WHERE login_id = :loginId AND is_deleted = FALSE",
"SELECT id, login_id, password, profile_url "
+ "FROM user_account "
+ "WHERE login_id = :loginId AND is_deleted = FALSE",
Map.of("loginId", loginId), (rs, rowNum) -> new UserAccount(rs.getInt("id"),
rs.getString("login_id"),
rs.getString("password"),
Expand All @@ -53,4 +57,17 @@ public List<UserAccount> findAll() {
rs.getString("login_id"),
rs.getString("profile_url")));
}

public Optional<UserAccount> findByEmail(String email) {
String sql = "SELECT id, login_id, profile_url FROM user_account WHERE email = :email AND is_deleted = FALSE";

MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("email", email);
return Optional.ofNullable(
DataAccessUtils.singleResult(jdbcTemplate.query(sql, params, (rs, rowNum) -> UserAccount.createUserProfile(
rs.getInt("id"),
rs.getString("login_id"),
rs.getString("profile_url")
))));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.codesquad.issuetracker.infrastructure.security.oauth;

import java.util.Arrays;
import java.util.Map;

import org.springframework.core.ParameterizedTypeReference;
Expand Down Expand Up @@ -86,9 +87,27 @@ private GithubUser getGithubUser(final String token) {
})
.blockOptional()
.orElseThrow(() -> new ApplicationException(ErrorCode.GITHUB_FAILED_LOGIN));
response.put("email", getEmail(token));
return new GithubUser(response);
}

private String getEmail(final String token) {
Map<String, Object>[] response = githubResourceClient
.get()
.uri("/user/emails")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>[]>() {
})
.blockOptional()
.orElseThrow(() -> new ApplicationException(ErrorCode.GITHUB_FAILED_GET_EMAIL));
return Arrays.stream(response)
.filter(elem -> elem.get("primary").toString().equals("true"))
.map(elem -> elem.get("email").toString())
.findFirst()
.orElseThrow(() -> new ApplicationException(ErrorCode.GITHUB_FAILED_GET_EMAIL));
}

public String getClientId() {
return clientId;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import kr.codesquad.issuetracker.application.AuthService;
import kr.codesquad.issuetracker.presentation.request.LoginRequest;
import kr.codesquad.issuetracker.presentation.request.OauthSignupRequest;
import kr.codesquad.issuetracker.presentation.request.SignupRequest;
import kr.codesquad.issuetracker.presentation.response.LoginSuccessResponse;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -48,4 +49,10 @@ public ResponseEntity<Void> oauthLogin() {
public ResponseEntity<LoginSuccessResponse> oauthLogin(@RequestParam("code") String code) {
return ResponseEntity.ok(authService.oauthLogin(code));
}

@PostMapping("/login/oauth/signup")
public ResponseEntity<LoginSuccessResponse> oauthSignup(@RequestBody OauthSignupRequest request) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(authService.oauthSignup(request.getEmail(), request.getUsername()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package kr.codesquad.issuetracker.presentation.request;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class OauthSignupRequest {

private String email;
private String username;
}
1 change: 1 addition & 0 deletions backend/src/main/resources/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS `issue_tracker`.`user_account`
`login_id` VARCHAR(45) NOT NULL,
`password` VARCHAR(512) NOT NULL,
`profile_url` VARCHAR(512) NOT NULL,
`email` VARCHAR(45) NULL UNIQUE,
`is_deleted` TINYINT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE = InnoDB;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ class GithubClientTest {
+ " \"error_description\": \"The client_id and/or client_secret passed are incorrect.\",\n"
+ " \"error_uri\": \"/apps/managing-oauth-apps/troubleshooting-oauth-app-access-token-request-errors/#incorrect-client-credentials\"\n"
+ "}";
private static final String USER_EMAIL_RESPONSE = "[\n"
+ " {\n"
+ " \"email\": \"[email protected]\",\n"
+ " \"verified\": true,\n"
+ " \"primary\": true,\n"
+ " \"visibility\": \"public\"\n"
+ " }\n"
+ "]";

@DisplayName("코드와 함께 서버로 요청을 보낼 때 사용자의 정보를 가져오는데 성공한다.")
@Test
Expand All @@ -73,6 +81,7 @@ void getOAuthUser() throws IOException {

setUpMockWebServer(mockGithubServer, ACCESS_TOKEN_RESPONSE);
setUpMockWebServer(mockGithubServer, USER_INFO_RESPONSE);
setUpMockWebServer(mockGithubServer, USER_EMAIL_RESPONSE);

GithubClient githubClient = new GithubClient(
new OauthProperties(
Expand All @@ -88,7 +97,7 @@ void getOAuthUser() throws IOException {
GithubUser oAuthUser = githubClient.getOAuthUser("code");

// then
assertThat(oAuthUser.getUsername()).isEqualTo("23Yong");
assertThat(oAuthUser.getEmail()).isEqualTo("[email protected]");
mockGithubServer.shutdown();
}

Expand Down

0 comments on commit 4e6a32c

Please sign in to comment.