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
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ public enum AuthRedisKey implements RedisKeys {
TOKEN_BLACKLIST("auth:token:blacklist:%s", -1),
PASSWORD_RESET_TOKEN("auth:password_reset:token:%s", PasswordResetConstants.TOKEN_TTL_MINUTES * 60),
PASSWORD_RESET_EMAIL("auth:password_reset:email:%s", PasswordResetConstants.TOKEN_TTL_MINUTES * 60),
SOCIAL_LINK_TOKEN("auth:social:link_token:%s", 180),
;
SOCIAL_LINK_TOKEN("auth:social:link_token:%s", 180);

private final String pattern;
private final int ttlSeconds;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,55 @@
package project.flipnote.auth.controller;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import project.flipnote.auth.controller.docs.AuthControllerDocs;
import project.flipnote.auth.model.ChangePasswordRequest;
import project.flipnote.auth.model.EmailVerificationConfirmRequest;
import project.flipnote.auth.model.EmailVerificationRequest;
import project.flipnote.auth.model.PasswordResetCreateRequest;
import project.flipnote.auth.model.PasswordResetRequest;
import project.flipnote.auth.model.TokenPair;
import project.flipnote.auth.model.UserLoginRequest;
import project.flipnote.auth.model.UserLoginResponse;
import project.flipnote.auth.model.UserRegisterRequest;
import project.flipnote.auth.model.UserRegisterResponse;
import project.flipnote.auth.service.AuthService;
import project.flipnote.common.security.dto.AuthPrinciple;
import project.flipnote.common.security.jwt.JwtConstants;
import project.flipnote.common.security.jwt.JwtProperties;
import project.flipnote.common.util.CookieUtil;
import project.flipnote.user.model.SocialLinksResponse;

@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/auth")
public class AuthController {
public class AuthController implements AuthControllerDocs {

private final AuthService authService;
private final JwtProperties jwtProperties;
private final CookieUtil cookieUtil;

@PostMapping("/register")
public ResponseEntity<UserRegisterResponse> register(@Valid @RequestBody UserRegisterRequest req) {
UserRegisterResponse res = authService.register(req);
return ResponseEntity.status(HttpStatus.CREATED).body(res);
}

@PostMapping("/login")
public ResponseEntity<UserLoginResponse> login(
@Valid @RequestBody UserLoginRequest req
Expand Down Expand Up @@ -111,4 +128,33 @@ public ResponseEntity<Void> resetPassword(

return ResponseEntity.noContent().build();
}

@PatchMapping("/password")
public ResponseEntity<Void> updatePassword(
@AuthenticationPrincipal AuthPrinciple userAuth,
@Valid @RequestBody ChangePasswordRequest req
) {
authService.changePassword(userAuth.authId(), req);

return ResponseEntity.noContent().build();
}

@GetMapping("/social-links")
public ResponseEntity<SocialLinksResponse> getSocialLinks(
@AuthenticationPrincipal AuthPrinciple userAuth
) {
SocialLinksResponse res = authService.getSocialLinks(userAuth.authId());

return ResponseEntity.ok(res);
}

@DeleteMapping("/social-links/{socialLinkId}")
public ResponseEntity<Void> deleteSocialLink(
@AuthenticationPrincipal AuthPrinciple userAuth,
@PathVariable("socialLinkId") Long socialLinkId
) {
authService.deleteSocialLink(userAuth.authId(), socialLinkId);

return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,22 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import project.flipnote.auth.constants.OAuthConstants;
import project.flipnote.auth.controller.docs.OAuthControllerDocs;
import project.flipnote.auth.exception.AuthErrorCode;
import project.flipnote.auth.model.AuthorizationRedirect;
import project.flipnote.auth.model.TokenPair;
import project.flipnote.auth.service.OAuthService;
import project.flipnote.common.config.ClientProperties;
import project.flipnote.common.exception.BizException;
import project.flipnote.common.security.dto.UserAuth;
import project.flipnote.common.security.dto.AuthPrinciple;
import project.flipnote.common.security.jwt.JwtConstants;
import project.flipnote.common.security.jwt.JwtProperties;
import project.flipnote.common.util.CookieUtil;

@Slf4j
@RequiredArgsConstructor
@RestController
public class OAuthController {
public class OAuthController implements OAuthControllerDocs {

private final OAuthService oAuthService;
private final ClientProperties clientProperties;
Expand All @@ -44,7 +45,7 @@ public class OAuthController {
public ResponseEntity<Void> redirectToProviderAuthorization(
@PathVariable("provider") String provider,
HttpServletRequest request,
@AuthenticationPrincipal UserAuth userAuth
@AuthenticationPrincipal AuthPrinciple userAuth
) {
AuthorizationRedirect authRedirect = oAuthService.getAuthorizationUri(provider, request, userAuth);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package project.flipnote.auth.controller.docs;

import org.springframework.http.ResponseEntity;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import project.flipnote.auth.model.ChangePasswordRequest;
import project.flipnote.auth.model.EmailVerificationConfirmRequest;
import project.flipnote.auth.model.EmailVerificationRequest;
import project.flipnote.auth.model.PasswordResetCreateRequest;
import project.flipnote.auth.model.PasswordResetRequest;
import project.flipnote.auth.model.UserLoginRequest;
import project.flipnote.auth.model.UserLoginResponse;
import project.flipnote.auth.model.UserRegisterRequest;
import project.flipnote.auth.model.UserRegisterResponse;
import project.flipnote.common.security.dto.AuthPrinciple;
import project.flipnote.user.model.SocialLinksResponse;

public interface AuthControllerDocs {

@Operation(summary = "회원가입")
ResponseEntity<UserRegisterResponse> register(UserRegisterRequest req);

@Operation(summary = "로그인")
ResponseEntity<UserLoginResponse> login(UserLoginRequest req);

@Operation(summary = "로그아웃", security = { @SecurityRequirement(name = "access-token") })
ResponseEntity<Void> logout();

@Operation(summary = "이메일 인증번호 전송")
ResponseEntity<Void> sendEmailVerificationCode(EmailVerificationRequest req);

@Operation(summary = "이메일 인증번호 확인")
ResponseEntity<Void> confirmEmailVerificationCode(EmailVerificationConfirmRequest req);

@Operation(summary = "토큰 갱신")
ResponseEntity<UserLoginResponse> refreshToken(String refreshToken);

@Operation(summary = "비밀번호 재설정 링크 전송")
ResponseEntity<Void> requestPasswordReset(PasswordResetCreateRequest req);

@Operation(summary = "비밀번호 재설정")
ResponseEntity<Void> resetPassword(PasswordResetRequest req);

@Operation(summary = "내 비밀번호 변경", security = { @SecurityRequirement(name = "access-token") })
ResponseEntity<Void> updatePassword(AuthPrinciple userAuth, ChangePasswordRequest req);

@Operation(summary = "내 소셜 연동 계정 목록 조회", security = { @SecurityRequirement(name = "access-token") })
ResponseEntity<SocialLinksResponse> getSocialLinks(AuthPrinciple userAuth);

@Operation(summary = "소셜 연동 해제", security = { @SecurityRequirement(name = "access-token") })
ResponseEntity<Void> deleteSocialLink(AuthPrinciple userAuth, Long socialLinkId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package project.flipnote.auth.controller.docs;

import org.springframework.http.ResponseEntity;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import project.flipnote.common.security.dto.AuthPrinciple;

@Tag(name = "OAuth", description = "OAuth API")
public interface OAuthControllerDocs {

@Operation(summary = "소셜 인증 URL로 리다이렉트")
ResponseEntity<Void> redirectToProviderAuthorization(
String provider,
HttpServletRequest request,
AuthPrinciple userAuth
);

@Operation(summary = "소셜 계정 연동 및 로그인")
ResponseEntity<Void> handleCallback(
String provider,
String code,
String state,
String codeVerifier,
HttpServletRequest request
);
}
14 changes: 14 additions & 0 deletions src/main/java/project/flipnote/auth/entity/AccountRole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package project.flipnote.auth.entity;

import java.util.Arrays;

public enum AccountRole {
USER, ADMIN;

public static AccountRole from(String role) {
return Arrays.stream(AccountRole.values())
.filter(r -> r.name().equalsIgnoreCase(role))
.findFirst()
.orElse(null);
}
}
8 changes: 8 additions & 0 deletions src/main/java/project/flipnote/auth/entity/AccountStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package project.flipnote.auth.entity;

import lombok.Getter;

@Getter
public enum AccountStatus {
ACTIVE, WITHDRAWN;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package project.flipnote.user.entity;
package project.flipnote.auth.entity;

import java.time.LocalDateTime;

Expand All @@ -21,19 +21,20 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import project.flipnote.user.entity.UserProfile;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
@Table(
name = "user_oauth_link",
name = "oauth_link",
indexes = {
@Index(name = "idx_provider_provider_id", columnList = "provider, providerId")
}
)
@Entity
public class UserOAuthLink {
public class OAuthLink {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand All @@ -46,17 +47,17 @@ public class UserOAuthLink {
private String providerId;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@JoinColumn(name = "auth_id", nullable = false)
private UserAuth userAuth;

@CreatedDate
@Column(updatable = false)
private LocalDateTime linkedAt;

@Builder
public UserOAuthLink(String provider, String providerId, User user) {
public OAuthLink(String provider, String providerId, UserAuth userAuth) {
this.provider = provider;
this.providerId = providerId;
this.user = user;
this.userAuth = userAuth;
}
}
76 changes: 76 additions & 0 deletions src/main/java/project/flipnote/auth/entity/UserAuth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package project.flipnote.auth.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import project.flipnote.common.entity.SoftDeletableEntity;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "user_auth")
@Entity
public class UserAuth extends SoftDeletableEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String email;

private String password;

@Column(unique = true, nullable = false)
private Long userId;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AccountStatus status;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AccountRole role;

@Column(nullable = false)
private long tokenVersion;

@Builder
public UserAuth(
String email,
String password,
Long userId
) {
this.email = email;
this.password = password;
this.userId = userId;
this.status = AccountStatus.ACTIVE;
this.role = AccountRole.USER;
this.tokenVersion = 0L;
}

public void withdraw() {
super.softDelete();

this.status = AccountStatus.WITHDRAWN;

increaseTokenVersion();
}

public void increaseTokenVersion() {
this.tokenVersion++;
}

public void changePassword(String password) {
this.password = password;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public enum AuthErrorCode implements ErrorCode {
INVALID_SOCIAL_LINK_TOKEN(HttpStatus.NOT_FOUND, "AUTH_010", "소셜 계정 연동 토큰이 유효하지 않거나 만료되었습니다."),
ALREADY_LINKED_SOCIAL_ACCOUNT(HttpStatus.CONFLICT, "AUTH_011", "이미 연동된 소셜 계정입니다."),
NOT_REGISTERED_SOCIAL_ACCOUNT(HttpStatus.NOT_FOUND, "AUTH_012", "가입되지 않은 소셜 계정입니다."),
INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH_013", "지원하지 않는 소셜 제공자입니다.");
INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH_013", "지원하지 않는 소셜 제공자입니다."),
SOCIAL_LINK_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH_014", "소셜 연동 계정이 존재하지 않습니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Loading
Loading