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

feat: 앱을 통한 로그인 #37 #38

Merged
merged 11 commits into from
Jan 17, 2024
1 change: 1 addition & 0 deletions backend/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jar {
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-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'

//flyway
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import site.timecapsulearchive.core.domain.auth.dto.request.SignUpRequest;
import site.timecapsulearchive.core.domain.auth.dto.request.TokenReIssueRequest;
import site.timecapsulearchive.core.domain.auth.dto.response.OAuthUrlResponse;
import site.timecapsulearchive.core.domain.auth.dto.response.TemporaryTokenResponse;
import site.timecapsulearchive.core.domain.auth.dto.response.TokenResponse;

@Validated
public interface AuthApi {

@Operation(
Expand Down Expand Up @@ -104,6 +103,26 @@ public interface AuthApi {
)
ResponseEntity<TemporaryTokenResponse> getTemporaryTokenResponseByGoogle();

@Operation(
summary = "다른 소셜 프로바이더의 앱으로 인증한 클라이언트 저장 후 토큰 반환",
description = "다른 소셜 프로바이더의 앱으로 인증한 클라이언트의 정보를 저장하고 임시 인증 토큰(1시간)을 반환한다.",
tags = {"auth"}
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "ok",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = TemporaryTokenResponse.class)
)
)
})
@GetMapping(
value = "/sign-up",
produces = {"application/json"}
)
ResponseEntity<TemporaryTokenResponse> signUpWithSocialProvider(SignUpRequest request);

@Operation(
summary = "액세스 토큰 재발급",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
package site.timecapsulearchive.core.domain.auth.api;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import site.timecapsulearchive.core.domain.auth.dto.request.SignUpRequest;
import site.timecapsulearchive.core.domain.auth.dto.request.TokenReIssueRequest;
import site.timecapsulearchive.core.domain.auth.dto.response.OAuthUrlResponse;
import site.timecapsulearchive.core.domain.auth.dto.response.TemporaryTokenResponse;
import site.timecapsulearchive.core.domain.auth.dto.response.TokenResponse;
import site.timecapsulearchive.core.domain.auth.service.TokenService;
import site.timecapsulearchive.core.domain.member.dto.mapper.MemberMapper;
import site.timecapsulearchive.core.domain.member.service.MemberService;

@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthApiController implements AuthApi {

private final TokenService tokenService;
private final MemberService memberService;
private final MemberMapper memberMapper;

@Override
public ResponseEntity<OAuthUrlResponse> getOAuth2KakaoUrl() {
Expand All @@ -40,10 +46,18 @@ public ResponseEntity<TemporaryTokenResponse> getTemporaryTokenResponseByGoogle(

@Override
public ResponseEntity<TokenResponse> reIssueAccessToken(
@RequestBody final TokenReIssueRequest request) {
@Valid @RequestBody final TokenReIssueRequest request) {
return ResponseEntity.ok(tokenService.reIssueToken(request.refreshToken()));
}

@Override
public ResponseEntity<TemporaryTokenResponse> signUpWithSocialProvider(
@RequestBody final SignUpRequest request) {
Long id = memberService.createMember(memberMapper.signUpRequestToEntity(request));

return ResponseEntity.ok(tokenService.createTemporaryToken(id));
}

@Override
public ResponseEntity<Void> sendVerificationMessage() {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package site.timecapsulearchive.core.domain.auth.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import site.timecapsulearchive.core.domain.auth.entity.SocialType;

@Schema(description = "소셜 프로바이더의 인증 아이디로 회원 인증 상태 체크")
public record CheckStatusRequest(

@Schema(description = "소셜 프로바이더 인증 아이디")
@NotBlank(message = "인증 아이디는 필수 입니다.")
String authId,

@Schema(description = "소셜 프로바이더 타입")
@NotNull(message = "소셜 프로바이더 타입은 필수입니다.")
SocialType socialType
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package site.timecapsulearchive.core.domain.auth.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import site.timecapsulearchive.core.domain.auth.entity.SocialType;

@Schema(description = "소셜 프로바이더의 인증 아이디로 로그인 요청")
public record SignUpRequest(

@Schema(description = "소셜 프로바이더 인증 아이디")
@NotBlank(message = "인증 아이디는 필수입니다.")
String authId,

@Schema(description = "사용자 이메일")
@NotBlank(message = "사용자 이메일은 필수입니다.")
@Email
String email,

@Schema(description = "사용자 프로필 url")
@NotBlank(message = "사용자 프로필 url은 필수입니다.")
String profileUrl,

@Schema(description = "소셜 프로바이더 타입")
@NotNull(message = "소셜 프로바이더 타입은 필수입니다.")
SocialType socialType
) {

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package site.timecapsulearchive.core.domain.auth.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.validation.annotation.Validated;

@Schema(description = "임시 인증 토큰")
@Validated
@Schema(description = "토큰 재발급 요청")
public record TokenReIssueRequest(

@Schema(description = "리프레시 토큰")
@NotBlank(message = "리프레시 토큰은 필수입니다.")
String refreshToken
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
import site.timecapsulearchive.core.domain.auth.dto.request.CheckStatusRequest;
import site.timecapsulearchive.core.domain.member.dto.reqeust.MemberDetailUpdateRequest;
import site.timecapsulearchive.core.domain.member.dto.response.MemberDetailResponse;
import site.timecapsulearchive.core.domain.member.dto.response.MemberStatusResponse;

@Validated
public interface MemberApi {

@Operation(
Expand Down Expand Up @@ -56,4 +56,26 @@ public interface MemberApi {
consumes = {"multipart/form-data"}
)
ResponseEntity<Void> updateMemberById(@ModelAttribute MemberDetailUpdateRequest request);

@Operation(
summary = "다른 OAuth2 프로바이더의 아이디로 앱 내의 유저의 인증 상태를 반환",
description = "Google, Kakao 프로바이더가 제공하는 유저 아이디로 앱 내의 인증 상태를 반환한다.",
tags = {"member"}
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "ok",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = MemberStatusResponse.class)
)
)
})
@GetMapping(
value = "/status",
produces = {"application/json"}
)
ResponseEntity<MemberStatusResponse> checkStatus(
CheckStatusRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package site.timecapsulearchive.core.domain.member.api;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import site.timecapsulearchive.core.domain.auth.dto.request.CheckStatusRequest;
import site.timecapsulearchive.core.domain.member.dto.reqeust.MemberDetailUpdateRequest;
import site.timecapsulearchive.core.domain.member.dto.response.MemberDetailResponse;
import site.timecapsulearchive.core.domain.member.dto.response.MemberStatusResponse;
import site.timecapsulearchive.core.domain.member.service.MemberService;

@RestController
@RequiredArgsConstructor
@RequestMapping("/")
public class MemberApiController implements MemberApi {

private final MemberService memberService;

@Override
public ResponseEntity<MemberDetailResponse> findMemberById() {
return null;
}

@Override
public ResponseEntity<Void> updateMemberById(MemberDetailUpdateRequest request) {
return null;
}

@Override
public ResponseEntity<MemberStatusResponse> checkStatus(
@Valid @RequestBody CheckStatusRequest request
) {
return ResponseEntity.ok(memberService.checkStatus(
request.authId(),
request.socialType())
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.stereotype.Component;
import site.timecapsulearchive.core.domain.auth.dto.oauth.OAuth2UserInfo;
import site.timecapsulearchive.core.domain.auth.dto.request.SignUpRequest;
import site.timecapsulearchive.core.domain.auth.entity.SocialType;
import site.timecapsulearchive.core.domain.member.entity.Member;

Expand All @@ -15,4 +16,13 @@ public Member OAuthToEntity(SocialType socialType, OAuth2UserInfo oAuth2UserInfo
.profileUrl(oAuth2UserInfo.getImageUrl())
.build();
}

public Member signUpRequestToEntity(SignUpRequest request) {
return Member.builder()
.authId(request.authId())
.email(request.email())
.profileUrl(request.profileUrl())
.socialType(request.socialType())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package site.timecapsulearchive.core.domain.member.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "사용자 상태 확인 응답")
public record MemberStatusResponse(

@Schema(description = "사용자 존재 여부")
Boolean isExist,

@Schema(description = "전화번호 인증 여부")
Boolean isVerified
) {

public static MemberStatusResponse empty() {
return new MemberStatusResponse(false, false);
}

public static MemberStatusResponse from(Boolean isVerified) {
return new MemberStatusResponse(true, isVerified);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,28 +58,39 @@ public class Member extends BaseEntity {

@Column(name = "is_verified", nullable = false)
private Boolean isVerified;

@Column(name = "auth_id", nullable = false)
private String authId;

@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Capsule> capsules;

@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List<GroupInvite> groupInvites;

@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List<MemberGroup> groups;

@OneToMany(mappedBy = "friend", cascade = CascadeType.ALL, orphanRemoval = true)
private List<MemberFriend> friends;

@OneToMany(mappedBy = "owner", cascade = CascadeType.ALL, orphanRemoval = true)
private List<FriendInvite> friendsRequests;

@OneToMany(mappedBy = "friend", cascade = CascadeType.ALL, orphanRemoval = true)
private List<FriendInvite> notifications;

@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List<History> histories;

@Builder
private Member(String profileUrl, SocialType socialType, String email) {
private Member(String profileUrl, SocialType socialType, String email, String authId) {
this.profileUrl = profileUrl;
this.nickname = "";
this.socialType = socialType;
this.email = email;
this.isVerified = false;
this.notificationEnabled = false;
this.authId = authId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package site.timecapsulearchive.core.domain.member.repository;

import static site.timecapsulearchive.core.domain.member.entity.QMember.member;

import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import site.timecapsulearchive.core.domain.auth.entity.SocialType;

@Repository
@RequiredArgsConstructor
public class MemberQueryRepository {

private final JPAQueryFactory query;

public Boolean findIsVerifiedByAuthIdAndSocialType(
String authId,
SocialType socialType
) {
return query.select(member.isVerified)
.from(member)
.where(member.authId.eq(authId).and(member.socialType.eq(socialType)))
.fetchOne();
}
}
Loading