Skip to content
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
2 changes: 2 additions & 0 deletions api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6'

implementation 'org.springframework.boot:spring-boot-starter-webflux'

runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.h2database:h2'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletExce
path.startsWith("/v3/api-docs") ||
path.startsWith("/docs") ||
path.startsWith("/api/v1/test/push") ||
path.startsWith("/api/v1/test/health")
path.startsWith("/api/v1/test/health") ||
path.startsWith("/api/v1/auth/google") ||
path.startsWith("/oauth/callback") ||
path.startsWith("/login/google") ||
path.startsWith("/login/oauth2/code/google")
;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,20 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
).permitAll()

.requestMatchers(
"/api/v1/auth/token"
"/api/v1/auth/token",
"/api/v1/auth/google"
).permitAll()

.requestMatchers(
"/api/v1/test/*"
).permitAll()

.requestMatchers(
"/login/google",
"/oauth/callback",
"/login/oauth2/code/google"
).permitAll()

.anyRequest().authenticated()
)
.formLogin(AbstractHttpConfigurer::disable)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.pinback.api.google.controller;

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 com.pinback.api.google.dto.request.GoogleLoginRequest;
import com.pinback.application.auth.usecase.AuthUsecase;
import com.pinback.application.google.dto.response.GoogleLoginResponse;
import com.pinback.application.google.usecase.GoogleUsecase;
import com.pinback.shared.dto.ResponseDto;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;

@Slf4j
@RestController
@RequestMapping("/api/v1/auth/google")
@RequiredArgsConstructor
@Tag(name = "Google", description = "구글 소셜 로그인 API")
public class GoogleLonginController {

private final GoogleUsecase googleUsecase;
private final AuthUsecase authUsecase;

@Operation(summary = "구글 소셜 로그인", description = "구글을 통한 소셜 로그인을 진행합니다")
@PostMapping()
public Mono<ResponseDto<GoogleLoginResponse>> googleLogin(
@Valid @RequestBody GoogleLoginRequest request
) {
return googleUsecase.getUserInfo(request.toCommand())
.flatMap(googleResponse -> {
return authUsecase.getInfoAndToken(googleResponse.email())
.map(loginResponse -> {
return ResponseDto.ok(loginResponse);
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.pinback.api.google.dto.request;

import com.pinback.application.google.dto.GoogleLoginCommand;

import jakarta.validation.constraints.NotNull;

public record GoogleLoginRequest(
@NotNull(message = "인가 코드(code)는 비어있을 수 없습니다.")
String code
) {
public GoogleLoginCommand toCommand() {
return new GoogleLoginCommand(code);
}
}
7 changes: 7 additions & 0 deletions api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ jwt:
issuer: ${ISSUER}

fcm: ${FCM_JSON}

google:
client-id: ${CLIENT_ID}
client-secret: ${CLIENT_SECRET}
redirect-uri: ${REDIRECT_URI}
token-uri: ${TOKEN_URI}
user-info-uri: ${USER_INFO_URI}
8 changes: 5 additions & 3 deletions application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
dependencies {
// 도메인 모듈 (핵심 비즈니스 로직)
api project(':domain')

// Spring Boot (애플리케이션 서비스를 위한 기본 스프링 기능)
implementation 'org.springframework.boot:spring-boot-starter'

// 트랜잭션 관리와 페이징을 위한 JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// Test용 H2 데이터베이스
testImplementation 'com.h2database:h2'

implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@
import com.pinback.application.auth.dto.SignUpResponse;
import com.pinback.application.auth.dto.TokenResponse;
import com.pinback.application.auth.service.JwtProvider;
import com.pinback.application.google.dto.response.GoogleLoginResponse;
import com.pinback.application.notification.port.in.SavePushSubscriptionPort;
import com.pinback.application.user.port.out.UserGetServicePort;
import com.pinback.application.user.port.out.UserSaveServicePort;
import com.pinback.application.user.port.out.UserValidateServicePort;
import com.pinback.domain.user.entity.User;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;

@Slf4j
@Service
@RequiredArgsConstructor
public class AuthUsecase {
Expand Down Expand Up @@ -46,4 +50,28 @@ public TokenResponse getToken(String email) {

return new TokenResponse(accessToken);
}

@Transactional(readOnly = true)
public Mono<GoogleLoginResponse> getInfoAndToken(String email) {
return userGetServicePort.findUserByEmail(email)
.flatMap(existingUser -> {
log.info("기존 사용자 로그인 성공: User ID {}", existingUser.getId());

//Access Token 발급
String accessToken = jwtProvider.createAccessToken(existingUser.getId());

return Mono.just(GoogleLoginResponse.loggedIn(
existingUser.getId(), existingUser.getEmail(), accessToken
));
})
.switchIfEmpty(Mono.defer(() -> {
log.info("신규 유저 - 임시 유저 생성");
User tempUser = User.createTempUser(email);

return userSaveServicePort.saveUser(tempUser)
.flatMap(savedUser -> {
return Mono.just(GoogleLoginResponse.tempLogin(savedUser.getId(), savedUser.getEmail()));
});
}));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.pinback.application.common.exception;

import com.pinback.shared.constant.ExceptionCode;
import com.pinback.shared.exception.ApplicationException;

public class GoogleApiException extends ApplicationException {
public GoogleApiException() {
super(ExceptionCode.GOOGLE_API_ERROR);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.pinback.application.common.exception;

import com.pinback.shared.constant.ExceptionCode;
import com.pinback.shared.exception.ApplicationException;

public class GoogleEmailMissingException extends ApplicationException {
public GoogleEmailMissingException() {
super(ExceptionCode.GOOGLE_EMAIL_MISSING);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.pinback.application.common.exception;

import com.pinback.shared.constant.ExceptionCode;
import com.pinback.shared.exception.ApplicationException;

public class GoogleTokenMissingException extends ApplicationException {
public GoogleTokenMissingException() {
super(ExceptionCode.GOOGLE_TOKEN_MISSING);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.pinback.application.google.dto;

public record GoogleLoginCommand(
String code
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.pinback.application.google.dto.response;

import org.springframework.lang.Nullable;

public record GoogleApiResponse(
String id,
String email,
Boolean verifiedEmail,
String name,
@Nullable String givenName,
@Nullable String familyName,
String picture
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.pinback.application.google.dto.response;

import java.util.UUID;

public record GoogleLoginResponse(
boolean isUser,
UUID userId,
String email,
String accessToken
) {
public static GoogleLoginResponse loggedIn(UUID userId, String email, String accessToken) {
return new GoogleLoginResponse(true, userId, email, accessToken);
}

public static GoogleLoginResponse tempLogin(UUID userId, String email) {
return new GoogleLoginResponse(false, userId, email, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.pinback.application.google.dto.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record GoogleTokenResponse(
String accessToken,
String expiresIn,
String tokenType,
String scope,
String idToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.pinback.application.google.dto.response;

public record GoogleUserInfoResponse(
String email
) {
public static GoogleUserInfoResponse from(String email) {
return new GoogleUserInfoResponse(email);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.pinback.application.google.port.out;

import com.pinback.application.google.dto.response.GoogleUserInfoResponse;

import reactor.core.publisher.Mono;

public interface GoogleOAuthPort {
Mono<GoogleUserInfoResponse> fetchUserInfo(String code);
}
Loading