Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a880b41
[FEAT] 구글 로그인을 위한 GoogleClient 추가
eunsol-an Mar 16, 2025
bf9202f
[FEAT] 구글 로그인 콜백 api, 앱용 api 추가
eunsol-an Mar 16, 2025
19c9da7
Merge pull request #164 from Next-Room/feature/google-login
eunsol-an Mar 17, 2025
e1a87cc
[REFACTOR] 구글 로그인 응답 구조 변경
eunsol-an Mar 17, 2025
b4b0d02
Merge pull request #165 from Next-Room/feature/google-login
eunsol-an Mar 17, 2025
02c6cb5
[FEAT] 넥스트룸 가입 절차 api 추가
eunsol-an Mar 19, 2025
aeb036e
[FEAT] SecurityConfig 수정
eunsol-an Mar 19, 2025
f8a24c0
Merge pull request #166 from Next-Room/feature/google-login
eunsol-an Mar 19, 2025
f9adca3
[FEAT] 업데이트 정보 수신 동의 여부 저장
eunsol-an Mar 25, 2025
33f0b70
Merge pull request #167 from Next-Room/feature/google-login
eunsol-an Mar 25, 2025
a38a12c
[FIX] client id 변경
eunsol-an Mar 27, 2025
47a7ce4
Merge pull request #168 from Next-Room/feature/google-login
eunsol-an Mar 27, 2025
211f722
[DOCS] 구글 로그인 관련 swagger 문서 수정
eunsol-an Apr 3, 2025
6b38f29
Merge pull request #169 from Next-Room/feature/google-login
eunsol-an Apr 3, 2025
1a440fc
[FIX] 구독 결제 보류 상태 대응
eunsol-an Apr 3, 2025
754dbb7
Merge pull request #170 from Next-Room/feature/rtdn-on-hold
eunsol-an Apr 3, 2025
7288b77
[CHORE] 비밀번호 조건 변경
eunsol-an Apr 10, 2025
21850ba
Merge pull request #171 from Next-Room/feature/password
eunsol-an Apr 10, 2025
6d080c4
[FIX] 힌트 수정 구조 변경
eunsol-an Apr 10, 2025
1e3de4b
Merge pull request #172 from Next-Room/feature/hint-edit-sub
eunsol-an Apr 10, 2025
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
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
Expand All @@ -33,7 +35,9 @@ dependencies {
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '3.0.5'

implementation 'com.google.apis:google-api-services-androidpublisher:v3-rev20231030-2.0.0'
implementation 'com.google.auth:google-auth-library-oauth2-http:1.19.0'
implementation 'com.google.auth:google-auth-library-oauth2-http:1.33.1'
implementation 'com.google.api-client:google-api-client:1.33.1'
implementation 'com.google.http-client:google-http-client-gson:1.40.1'

implementation group: 'software.amazon.awssdk', name: 'bom', version: '2.28.4'
implementation group: 'software.amazon.awssdk', name: 's3', version: '2.28.4'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@
import com.nextroom.nextRoomServer.dto.DataResponse;
import com.nextroom.nextRoomServer.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
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.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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 org.springframework.web.bind.annotation.*;

@Tag(name = "Auth")
@RestController
Expand All @@ -30,26 +27,67 @@ public class AuthController {
summary = "회원가입",
responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "403", description = "Unauthorized / Invalid Token")
@ApiResponse(responseCode = "403", description = "Unauthorized / Invalid Token", content = @Content)
}
)
@PostMapping("/signup")
public ResponseEntity<BaseResponse> signUp(@RequestBody @Valid AuthDto.SignUpRequestDto request) {
public ResponseEntity<DataResponse<AuthDto.SignUpResponseDto>> signUp(@RequestBody @Valid AuthDto.SignUpRequestDto request) {
return ResponseEntity.ok(new DataResponse<>(OK, authService.signUp(request)));
}

@Operation(
summary = "로그인",
responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "403", description = "Unauthorized / Invalid Token")
@ApiResponse(responseCode = "403", description = "Unauthorized / Invalid Token", content = @Content)
}
)
@PostMapping("/login")
public ResponseEntity<BaseResponse> logIn(@RequestBody @Valid AuthDto.LogInRequestDto request) {
public ResponseEntity<DataResponse<AuthDto.LogInResponseDto>> logIn(@RequestBody @Valid AuthDto.LogInRequestDto request) {
return ResponseEntity.ok(new DataResponse<>(OK, authService.login(request)));
}

@Operation(
summary = "구글 로그인(웹용)",
description = "request: code",
responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "403", description = "Unauthorized / Invalid Token", content = @Content)
}
)
@GetMapping("/login/google/callback")
public ResponseEntity<DataResponse<AuthDto.LogInResponseDto>> googleLogIn(@RequestParam String code) {
AuthDto.GoogleLogInRequestDto request = new AuthDto.GoogleLogInRequestDto();
request.setCode(code);
return ResponseEntity.ok(new DataResponse<>(OK, authService.googleLogin(request)));
}

@Operation(
summary = "구글 로그인(앱용)",
description = "request: idToken",
responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "403", description = "Unauthorized / Invalid Token", content = @Content)
}
)
@PostMapping("/login/google/app")
public ResponseEntity<DataResponse<AuthDto.LogInResponseDto>> googleLogInApp(@RequestBody @Valid AuthDto.GoogleLogInRequestDto request) {
return ResponseEntity.ok(new DataResponse<>(OK, authService.googleLogin(request)));
}

@Operation(
summary = "넥스트룸 가입 절차",
responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "401", description = "TOKEN_UNAUTHORIZED", content = @Content),
@ApiResponse(responseCode = "404", description = "TARGET_SHOP_NOT_FOUND", content = @Content)
}
)
@PutMapping("/shop")
public ResponseEntity<DataResponse<AuthDto.ShopUpdateResponseDto>> updateShopInfo(@RequestBody @Valid AuthDto.ShopUpdateRequestDto request) {
return ResponseEntity.ok(new DataResponse<>(OK, authService.updateShopInfo(request)));
}

@Operation(
summary = "토큰 재발급",
responses = {
Expand Down
27 changes: 25 additions & 2 deletions src/main/java/com/nextroom/nextRoomServer/domain/Shop.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Objects;
import java.util.Optional;

import com.nextroom.nextRoomServer.dto.AuthDto;
import com.nextroom.nextRoomServer.enums.UserStatus;
import com.nextroom.nextRoomServer.exceptions.CustomException;
import com.nextroom.nextRoomServer.security.SecurityUtil;
Expand Down Expand Up @@ -46,22 +47,31 @@ public class Shop extends Timestamped {
@Column
private String email;

@Column
private String googleSub;

@Column(nullable = false, length = 5)
private String adminCode;

@Column(nullable = false)
@Column
private String password;

@Column(nullable = false)
@Column
private String name;

@Comment(value = "1: 웹(홈페이지)에서 PC로 들어온 유저, 2: 웹(홈페이지)에서 모바일로 들어온 유저, 3: 앱에서 들어온 유저")
@Column
private Integer type;

@Column
private String signupSource;

@Column
private String comment;

@Column
private Boolean adsConsent;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Authority authority;
Expand Down Expand Up @@ -105,4 +115,17 @@ public void setAllUseTimerUrl(boolean active) {
this.themes.forEach(theme -> Optional.ofNullable(theme.getTimerImageUrl())
.ifPresent(it -> theme.setUseTimerUrl(active)));
}

public boolean isCompleteSignUp() {
return this.name != null && !this.name.isEmpty();
}

public void updateShopInfo(AuthDto.ShopUpdateRequestDto request) {
this.name = request.getName();
this.signupSource = request.getSignupSource();
this.comment = request.getComment();
this.type = request.getType();
this.adsConsent = request.getAdsConsent();
this.lastLoginAt = LocalDateTime.now();
}
}
162 changes: 143 additions & 19 deletions src/main/java/com/nextroom/nextRoomServer/dto/AuthDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,34 @@

import java.util.Collections;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;

import com.nextroom.nextRoomServer.domain.Authority;
import com.nextroom.nextRoomServer.domain.Shop;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import static com.nextroom.nextRoomServer.util.Timestamped.dateTimeFormatter;

public class AuthDto {
private static final String ADMIN_CODE_REGEX = "[0-9]{5}";
private static final String PASSWORD_CONDITION_MIN_LENGTH_REGEX = ".{8,}";
private static final String PASSWORD_CONDITION_LOWER_CASE_REGEX = ".*[a-z].*";
private static final String PASSWORD_CONDITION_UPPER_CASE_REGEX = ".*[A-Z].*";
private static final String PASSWORD_CONDITION_NUMBER_REGEX = ".*[0-9].*";
private static final String PASSWORD_CONDITION_SPECIAL_CHARACTER_REGEX = ".*[!@#$%^&*()].*";
private static final String PASSWORD_CONDITION_NUMBER_OR_SPECIAL_CHAR_REGEX = ".*[0-9!@#$%^&*()+=?-].*";

private static final String NO_NAME = "오픈 예정 매장";

@Getter
Expand All @@ -40,10 +43,7 @@ public static class SignUpRequestDto {
@Setter
@NotEmpty(message = "비밀번호를 입력해 주세요.")
@Pattern(regexp = PASSWORD_CONDITION_MIN_LENGTH_REGEX, message = "비밀번호는 최소 8자리 이상이어야 합니다.")
@Pattern(regexp = PASSWORD_CONDITION_LOWER_CASE_REGEX, message = "비밀번호에 영문 소문자를 최소 1개 이상 포함해야 합니다.")
@Pattern(regexp = PASSWORD_CONDITION_UPPER_CASE_REGEX, message = "비밀번호에 영문 대문자를 최소 1개 이상 포함해야 합니다.")
@Pattern(regexp = PASSWORD_CONDITION_NUMBER_REGEX, message = "비밀번호에 숫자(0-9)를 최소 1개 이상 포함해야 합니다.")
@Pattern(regexp = PASSWORD_CONDITION_SPECIAL_CHARACTER_REGEX, message = "비밀번호에 특수문자(!, @, #, $, %, ^, &, *, (, ))를 최소 1개 이상 포함해야 합니다.")
@Pattern(regexp = PASSWORD_CONDITION_NUMBER_OR_SPECIAL_CHAR_REGEX, message = "비밀번호에 숫자(0-9) 혹은 특수문자(!, @, #, $, %, ^, &, *, (, ), +, =, ?, -)를 최소 1개 이상 포함해야 합니다.")
private String password;
@NotBlank(message = "업체명을 입력해 주세요.")
private String name;
Expand Down Expand Up @@ -75,6 +75,15 @@ public static class SignUpResponseDto {
private String adminCode;
private String createdAt;
private String modifiedAt;

public static AuthDto.SignUpResponseDto toSignUpResponseDto(Shop shop) {
return SignUpResponseDto.builder()
.email(shop.getEmail())
.name(shop.getName())
.adminCode(shop.getAdminCode())
.createdAt(dateTimeFormatter(shop.getCreatedAt()))
.modifiedAt(dateTimeFormatter(shop.getModifiedAt())).build();
}
}

@Getter
Expand All @@ -99,23 +108,129 @@ public UsernamePasswordAuthenticationToken toAuthentication() {
@Getter
@Builder
public static class LogInResponseDto {
@NotBlank
private String shopName;
@NotBlank
private String adminCode;
@NotBlank
private String grantType;
@NotBlank
private String accessToken;
@NotNull
private long accessTokenExpiresIn;
@NotBlank
private String refreshToken;

public static AuthDto.LogInResponseDto toLogInResponseDto(String shopName, String adminCode,
TokenDto tokenDto) {
return new LogInResponseDtoBuilder()
.shopName(shopName)
.adminCode(adminCode)
.grantType(tokenDto.getGrantType())
.accessToken(tokenDto.getAccessToken())
.accessTokenExpiresIn(tokenDto.getAccessTokenExpiresIn())
.refreshToken(tokenDto.getRefreshToken())
.build();
@NotNull
@Schema(description = "넥스트룸 가입 절차 완료 여부")
private Boolean isComplete;

public static AuthDto.LogInResponseDto toLogInResponseDto(Shop shop, TokenDto tokenDto) {
return LogInResponseDto.builder()
.isComplete(shop.isCompleteSignUp())
.shopName(shop.getName())
.adminCode(shop.getAdminCode())
.grantType(tokenDto.getGrantType())
.accessToken(tokenDto.getAccessToken())
.accessTokenExpiresIn(tokenDto.getAccessTokenExpiresIn())
.refreshToken(tokenDto.getRefreshToken())
.build();
}
}

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(force = true)
public static class ShopUpdateRequestDto {
@NotBlank(message = "매장명을 입력해 주세요.")
@Schema(description = "매장명")
private String name;
@NotBlank(message = "가입 경로를 입력해 주세요.")
@Schema(description = "가입 경로(ex. 네이버 검색)")
private String signupSource;
@Schema(description = "가입 이유(ex. 운영 중 매장 도입)")
private String comment;
@Schema(description = "가입 경로 타입 (1: 웹(홈페이지)에서 PC로 들어온 유저\n" +
"2: 웹(홈페이지)에서 모바일로 들어온 유저\n" +
"3: 앱에서 들어온 유저)")
private Integer type;
@Schema(description = "업데이트 소식 수신 동의 여부")
private Boolean adsConsent;
}

@Getter
@Builder
public static class ShopUpdateResponseDto {
@NotNull
@Schema(description = "넥스트룸 가입 절차 완료 여부")
private Boolean isComplete;
@NotBlank
@Schema(description = "매장명")
private String shopName;
@NotBlank
@Schema(description = "관리자 코드")
private String adminCode;

public static AuthDto.ShopUpdateResponseDto toShopUpdateResponseDto(Shop shop) {
return ShopUpdateResponseDto.builder()
.isComplete(shop.isCompleteSignUp())
.shopName(shop.getName())
.adminCode(shop.getAdminCode())
.build();
}
}

@Getter
@Setter
public static class GoogleLogInRequestDto {
private String code;
@NotBlank(message = "ID TOKEN IS NOT NULL")
private String idToken;
public boolean isCode() {
return this.code != null;
}
}

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(force = true)
@JsonIgnoreProperties(ignoreUnknown = true)
public static class GoogleTokenResponseDto {
@JsonProperty("access_token")
private String accessToken;

@JsonProperty("id_token")
private String idToken;

@JsonProperty("token_type")
private String tokenType;

@JsonProperty("expires_in")
private String expiresIn;

@JsonProperty("scope")
private String scope;
}

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(force = true)
@JsonIgnoreProperties(ignoreUnknown = true)
public static class GoogleInfoResponseDto {
@JsonProperty("id")
private String id;

@JsonProperty("email")
private String email;

public static AuthDto.GoogleInfoResponseDto toGoogleInfoResponseDto(String id, String email) {
return GoogleInfoResponseDto.builder()
.id(id)
.email(email)
.build();
}
}

Expand All @@ -135,5 +250,14 @@ public static class ReissueResponseDto {
private String accessToken;
private long accessTokenExpiresIn;
private String refreshToken;

public static AuthDto.ReissueResponseDto toReissueResponseDto(TokenDto tokenDto) {
return ReissueResponseDto.builder()
.grantType(tokenDto.getGrantType())
.accessToken(tokenDto.getAccessToken())
.accessTokenExpiresIn(tokenDto.getAccessTokenExpiresIn())
.refreshToken(tokenDto.getRefreshToken())
.build();
}
}
}
Loading
Loading