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
4 changes: 2 additions & 2 deletions .github/workflows/develop_build_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ jobs:
done
echo "✅ postgres is ready!"

echo "${{ secrets.SCHEMA_SQL }}" | docker exec -i $TEST_POSTGRES_CONTAINER_NAME psql -U $TEST_POSTGRES_USER -d $TEST_POSTGRES_DB
echo "${{ secrets.DATA_SQL }}" | docker exec -i $TEST_POSTGRES_CONTAINER_NAME psql -U $TEST_POSTGRES_USER -d $TEST_POSTGRES_DB
echo "${{ secrets.SCHEMA_SQL }}" | base64 --decode | docker exec -i $TEST_POSTGRES_CONTAINER_NAME psql -U $TEST_POSTGRES_USER -d $TEST_POSTGRES_DB
echo "${{ secrets.DATA_SQL }}" | base64 --decode | docker exec -i $TEST_POSTGRES_CONTAINER_NAME psql -U $TEST_POSTGRES_USER -d $TEST_POSTGRES_DB

# Gradlew 실행 권한 허용
- name: Grant Execute Permission for Gradlew
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/develop_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ jobs:
done
echo "✅ postgres is ready!"

echo "${{ secrets.SCHEMA_SQL }}" | docker exec -i $TEST_POSTGRES_CONTAINER_NAME psql -U $TEST_POSTGRES_USER -d $TEST_POSTGRES_DB
echo "${{ secrets.DATA_SQL }}" | docker exec -i $TEST_POSTGRES_CONTAINER_NAME psql -U $TEST_POSTGRES_USER -d $TEST_POSTGRES_DB
echo "${{ secrets.SCHEMA_SQL }}" | base64 --decode | docker exec -i $TEST_POSTGRES_CONTAINER_NAME psql -U $TEST_POSTGRES_USER -d $TEST_POSTGRES_DB
echo "${{ secrets.DATA_SQL }}" | base64 --decode | docker exec -i $TEST_POSTGRES_CONTAINER_NAME psql -U $TEST_POSTGRES_USER -d $TEST_POSTGRES_DB

# Gradlew 실행 권한 허용
- name: Grant Execute Permission for Gradlew
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ tasks.register('addAuthorization', Task) {
def securitySchemesContent = " securitySchemes:\n" +
" APIKey:\n" +
" type: apiKey\n" +
" name: JSESSIONID\n" +
" name: SESSION\n" +
" in: cookie\n" +
"security:\n" +
" - APIKey: [] # Apply the security scheme here"
Expand Down
19 changes: 19 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ Content-Type: application/json
| 공통 | 500 | INTERNAL_SERVER_ERROR | E500_001 | 서버 측에서 처리하지 못한 예외가 발생하면 모든 api 요청에 대해 공통적으로 반환됨.
|===



== 회원

=== **1. 이메일 중복 확인**
Expand Down Expand Up @@ -161,3 +163,20 @@ include::{snippetsDir}/emailCodeVerification/1/http-response.adoc[]
include::{snippetsDir}/emailCodeVerification/1/response-fields.adoc[]


== 인증/인가

=== **1. 유저 로그인**

유저 로그인 api 입니다. (이메일, 패스워드)

=== Request
include::{snippetsDir}/loginUser/1/http-request.adoc[]

== 성공 Response
include::{snippetsDir}/loginUser/1/http-response.adoc[]

== Response Body Fields
include::{snippetsDir}/loginUser/1/response-fields.adoc[]

== 실패 Response
include::{snippetsDir}/loginUser/2/http-response.adoc[]
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.ftm.server.adapter.controller.auth;

import com.ftm.server.adapter.dto.request.UserLoginRequest;
import com.ftm.server.adapter.dto.response.UserLoginResponse;
import com.ftm.server.common.response.ApiResponse;
import com.ftm.server.common.response.enums.SuccessResponseCode;
import com.ftm.server.domain.dto.command.UserLoginCommand;
import com.ftm.server.domain.usecase.auth.UserLoginUseCase;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class UserLoginController {

private final UserLoginUseCase loginUseCase;

@PostMapping("/api/auth/login")
public ResponseEntity<ApiResponse<UserLoginResponse>> login(
@RequestBody UserLoginRequest request,
HttpServletRequest req,
HttpServletResponse res) {
return ResponseEntity.status(HttpStatus.OK)
.body(
ApiResponse.success(
SuccessResponseCode.OK,
UserLoginResponse.from(
loginUseCase.login(
UserLoginCommand.from(request), req, res))));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ftm.server.adapter.dto.request;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class UserLoginRequest {

private final String email;
private final String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.ftm.server.adapter.dto.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.ftm.server.domain.dto.vo.UserSummaryVo;
import java.time.LocalDateTime;
import lombok.Getter;

@Getter
public class UserLoginResponse {

private final Long id;
private final String nickname;
private final String profileImageUrl;
private final String mildLevelName;
private final String spicyLevelName;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm", shape = JsonFormat.Shape.STRING)
private final LocalDateTime loginTime;

UserLoginResponse(UserSummaryVo userSummaryVo) {
this.id = userSummaryVo.getId();
this.nickname = userSummaryVo.getNickname();
this.profileImageUrl = userSummaryVo.getProfileImageUrl();
this.mildLevelName = userSummaryVo.getMildLevelName();
this.spicyLevelName = userSummaryVo.getSpicyLevelName();
this.loginTime = LocalDateTime.now();
}

public static UserLoginResponse from(UserSummaryVo userSummaryVo) {
return new UserLoginResponse(userSummaryVo);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ftm.server.adapter.gateway;

import com.ftm.server.domain.dto.command.UserLoginCommand;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;

/** 시큐리티 인증 관련 작업 Gateway */
public interface AuthenticationGateway {

// 일반 유저 인증 객체 생성
Authentication createAuthenticationFromCredentials(UserLoginCommand command);

// 인증 세션 등록 (시큐리티 컨텍스트 저장)
void saveAuthenticatedSession(
Authentication authentication,
HttpServletRequest request,
HttpServletResponse response);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ftm.server.adapter.gateway.repository;

import com.ftm.server.entity.entities.UserImage;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserImageRepository extends JpaRepository<UserImage, Long> {

Optional<UserImage> findByUserId(Long userId);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.ftm.server.adapter.gateway.repository;

import com.ftm.server.entity.entities.User;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {

Boolean existsByEmail(String email);

Optional<User> findByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum ErrorResponseCode {

// 401번
NOT_AUTHENTICATED(HttpStatus.UNAUTHORIZED, "E401_001", "인증되지 않은 사용자입니다."),
INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "E401_002", "인증에 실패하였습니다."),

// 403번
NOT_AUTHORIZATION(HttpStatus.FORBIDDEN, "E403_001", "인증된 사용자이나 해당 자원에 대한 접근 권한이 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ftm.server.domain.dto.command;

import com.ftm.server.adapter.dto.request.UserLoginRequest;
import lombok.Getter;

@Getter
public class UserLoginCommand {

private final String email;
private final String password;

private UserLoginCommand(String email, String password) {
this.email = email;
this.password = password;
}

public static UserLoginCommand from(UserLoginRequest request) {
return new UserLoginCommand(request.getEmail(), request.getPassword());
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/ftm/server/domain/dto/query/FindByIdQuery.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ftm.server.domain.dto.query;

import lombok.Getter;

@Getter
public class FindByIdQuery {

private final Long id;

private FindByIdQuery(Long id) {
this.id = id;
}

public static FindByIdQuery of(Long id) {
return new FindByIdQuery(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ftm.server.domain.dto.query;

import lombok.Getter;

@Getter
public class FindByUserIdQuery {

private final Long userId;

private FindByUserIdQuery(Long userId) {
this.userId = userId;
}

public static FindByUserIdQuery of(Long userId) {
return new FindByUserIdQuery(userId);
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/ftm/server/domain/dto/vo/UserSummaryVo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.ftm.server.domain.dto.vo;

import com.ftm.server.entity.entities.GroomingLevel;
import com.ftm.server.entity.entities.User;
import com.ftm.server.entity.entities.UserImage;
import lombok.Getter;

@Getter
public class UserSummaryVo {

private final Long id;
private final String nickname;
private final String profileImageUrl;
private final String mildLevelName;
private final String spicyLevelName;

private UserSummaryVo(User user, UserImage userImage, GroomingLevel groomingLevel) {
this.id = user.getId();
this.nickname = user.getNickname();
this.profileImageUrl = userImage.getObjectKey(); // TODO: 추후 CDN 주소 + getObjectKey() 로 변경해야함
this.mildLevelName = groomingLevel.getMildLevelName();
this.spicyLevelName = groomingLevel.getSpicyLevelName();
}

public static UserSummaryVo of(User user, UserImage userImage) {
return new UserSummaryVo(user, userImage, user.getGroomingLevel());
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/ftm/server/domain/service/UserImageService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ftm.server.domain.service;

import com.ftm.server.adapter.gateway.repository.UserImageRepository;
import com.ftm.server.domain.dto.query.FindByUserIdQuery;
import com.ftm.server.entity.entities.UserImage;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserImageService {

private final UserImageRepository userImageRepository;

public UserImage queryUserImageByUserId(FindByUserIdQuery query) {
return userImageRepository
.findByUserId(query.getUserId())
.orElseThrow(() -> new RuntimeException(""));
}
}
11 changes: 9 additions & 2 deletions src/main/java/com/ftm/server/domain/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
package com.ftm.server.domain.service;

import com.ftm.server.adapter.gateway.repository.UserRepository;
import com.ftm.server.common.exception.CustomException;
import com.ftm.server.domain.dto.query.FindByEmailQuery;
import com.ftm.server.domain.dto.query.FindByIdQuery;
import com.ftm.server.domain.dto.vo.EmailDuplicationVo;
import com.ftm.server.entity.entities.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {

private final UserRepository userRepository;

public EmailDuplicationVo isEmailDuplicated(FindByEmailQuery query) {
return EmailDuplicationVo.of(userRepository.existsByEmail(query.getEmail()));
}

public User queryUser(FindByIdQuery query) {
return userRepository
.findById(query.getId())
.orElseThrow(() -> CustomException.USER_NOT_FOUND);
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.ftm.server.domain.usecase.auth;

import com.ftm.server.adapter.gateway.AuthenticationGateway;
import com.ftm.server.common.annotation.UseCase;
import com.ftm.server.common.exception.CustomException;
import com.ftm.server.common.response.enums.ErrorResponseCode;
import com.ftm.server.domain.dto.command.UserLoginCommand;
import com.ftm.server.domain.dto.query.FindByIdQuery;
import com.ftm.server.domain.dto.query.FindByUserIdQuery;
import com.ftm.server.domain.dto.vo.UserSummaryVo;
import com.ftm.server.domain.service.UserImageService;
import com.ftm.server.domain.service.UserService;
import com.ftm.server.entity.entities.User;
import com.ftm.server.entity.entities.UserImage;
import com.ftm.server.infrastructure.security.UserPrincipal;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.transaction.annotation.Transactional;

@UseCase
@RequiredArgsConstructor
public class UserLoginUseCase {

private final AuthenticationGateway securityAuthenticateGateway;
private final UserService userService;
private final UserImageService userImageService;

@Transactional
public UserSummaryVo login(
UserLoginCommand command, HttpServletRequest req, HttpServletResponse res) {

// 인증을 수행하고 인증 객체 생성 (실패 시 예외 발생)
Authentication auth = createAuthenticationOrThrow(command);
UserPrincipal userPrincipal = (UserPrincipal) auth.getPrincipal();

User user = userService.queryUser(FindByIdQuery.of(userPrincipal.getId()));
UserImage userImage =
userImageService.queryUserImageByUserId(FindByUserIdQuery.of(user.getId()));

// 인증 세션 등록 (시큐리티 컨텍스트 등록)
securityAuthenticateGateway.saveAuthenticatedSession(auth, req, res);

return UserSummaryVo.of(user, userImage);
}

private Authentication createAuthenticationOrThrow(UserLoginCommand command) {
try {
return securityAuthenticateGateway.createAuthenticationFromCredentials(command);
} catch (AuthenticationException ex) {
throw new CustomException(ErrorResponseCode.INVALID_CREDENTIALS);
}
}
}
Loading