From 4645c65dd8fa5191f404b7c80fde3bf31426d12e Mon Sep 17 00:00:00 2001 From: qqwwee771441 <104188251+qqwwee771441@users.noreply.github.com> Date: Wed, 9 Apr 2025 00:55:00 +0900 Subject: [PATCH 01/11] fix: .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c2065bc..d48f7ac 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +src/main/resources/application.properties From e21ac52486229796b5e8b8e50c908f72d5fdb4df Mon Sep 17 00:00:00 2001 From: qqwwee771441 <104188251+qqwwee771441@users.noreply.github.com> Date: Wed, 9 Apr 2025 01:12:51 +0900 Subject: [PATCH 02/11] feat: hw1 --- build.gradle | 53 ++++++---- .../devlog/common/base/BaseEntity.java | 24 +++++ .../auth/controller/AuthController.java | 41 +++++++ .../domain/auth/dto/UserLoginRequestDto.java | 18 ++++ .../domain/auth/dto/UserLoginResponseDto.java | 11 ++ .../domain/auth/dto/UserLogoutRequestDto.java | 14 +++ .../auth/dto/UserRefreshRequestDto.java | 14 +++ .../auth/dto/UserRefreshResponseDto.java | 11 ++ .../domain/auth/dto/UserSignupRequestDto.java | 51 +++++++++ .../auth/dto/UserSignupResponseDto.java | 13 +++ .../domain/auth/service/AuthService.java | 78 ++++++++++++++ .../controller/OAuth2PkceController.java | 24 +++++ .../oauth2/dto/OAuth2CallbackRequestDto.java | 12 +++ .../oauth2/dto/OAuth2CallbackResponseDto.java | 11 ++ .../oauth2/handler/OAuth2FailureHandler.java | 34 ++++++ .../oauth2/handler/OAuth2SuccessHandler.java | 40 +++++++ .../domain/oauth2/model/OAuth2Attributes.java | 81 ++++++++++++++ .../service/CustomOAuth2UserService.java | 42 ++++++++ .../oauth2/service/OAuth2PkceService.java | 100 ++++++++++++++++++ .../user/controller/UserController.java | 25 +++++ .../user/dto/UserProfileRequestDto.java | 12 +++ .../user/dto/UserProfileResponseDto.java | 24 +++++ .../devlog/domain/user/entity/User.java | 68 ++++++++++++ .../devlog/domain/user/enums/Gender.java | 5 + .../devlog/domain/user/enums/Provider.java | 5 + .../devlog/domain/user/enums/Role.java | 5 + .../user/repository/UserRepository.java | 12 +++ .../domain/user/service/UserService.java | 21 ++++ .../devlog/global/annotation/InjectEmail.java | 9 ++ .../devlog/global/annotation/InjectToken.java | 9 ++ .../devlog/global/config/JpaConfig.java | 9 ++ .../devlog/global/config/WebConfig.java | 30 ++++++ .../resolver/InjectEmailArgumentResolver.java | 77 ++++++++++++++ .../resolver/InjectTokenArgumentResolver.java | 66 ++++++++++++ .../global/response/api/ApiResponse.java | 21 ++++ .../error/exception/CustomException.java | 21 ++++ .../exception/DuplicateEmailException.java | 9 ++ .../InvalidEmailOrPasswordException.java | 9 ++ .../exception/InvalidPasswordException.java | 9 ++ .../exception/InvalidProviderException.java | 9 ++ .../InvalidRefreshTokenException.java | 9 ++ .../InvalidStateFormatException.java | 9 ++ .../exception/InvalidTokenException.java | 9 ++ .../OAuth2TokenRequestException.java | 9 ++ .../exception/OAuth2UserInfoException.java | 9 ++ .../TokenInjectionFailedException.java | 9 ++ .../UnauthenticatedUserException.java | 9 ++ .../exception/UserNotFoundException.java | 9 ++ .../error/handler/GlobalExceptionHandler.java | 72 +++++++++++++ .../response/error/model/ErrorCode.java | 29 +++++ .../response/error/model/ErrorResponse.java | 12 +++ .../security/config/SecurityConfig.java | 91 ++++++++++++++++ .../filter/JwtAuthenticationFilter.java | 44 ++++++++ .../global/security/jwt/JwtTokenProvider.java | 75 +++++++++++++ .../service/CustomUserDetailsService.java | 21 ++++ .../redis/config/RedisConfig.java | 36 +++++++ .../redis/repository/RedisRepository.java | 49 +++++++++ .../testconfig/redis/RedisInitializer.java | 18 ++++ src/main/resources/application.properties | 89 ++++++++++++++++ .../auth/controller/AuthControllerTest.java | 4 + .../domain/auth/service/AuthServiceTest.java | 4 + .../handler/OAuth2FailureHandlerTest.java | 4 + .../handler/OAuth2SuccessHandlerTest.java | 4 + .../service/CustomOAuth2UserServiceTest.java | 4 + .../user/controller/UserControllerTest.java | 4 + .../domain/user/service/UserServiceTest.java | 4 + 66 files changed, 1745 insertions(+), 18 deletions(-) create mode 100644 src/main/java/apptive/devlog/common/base/BaseEntity.java create mode 100644 src/main/java/apptive/devlog/domain/auth/controller/AuthController.java create mode 100644 src/main/java/apptive/devlog/domain/auth/dto/UserLoginRequestDto.java create mode 100644 src/main/java/apptive/devlog/domain/auth/dto/UserLoginResponseDto.java create mode 100644 src/main/java/apptive/devlog/domain/auth/dto/UserLogoutRequestDto.java create mode 100644 src/main/java/apptive/devlog/domain/auth/dto/UserRefreshRequestDto.java create mode 100644 src/main/java/apptive/devlog/domain/auth/dto/UserRefreshResponseDto.java create mode 100644 src/main/java/apptive/devlog/domain/auth/dto/UserSignupRequestDto.java create mode 100644 src/main/java/apptive/devlog/domain/auth/dto/UserSignupResponseDto.java create mode 100644 src/main/java/apptive/devlog/domain/auth/service/AuthService.java create mode 100644 src/main/java/apptive/devlog/domain/oauth2/controller/OAuth2PkceController.java create mode 100644 src/main/java/apptive/devlog/domain/oauth2/dto/OAuth2CallbackRequestDto.java create mode 100644 src/main/java/apptive/devlog/domain/oauth2/dto/OAuth2CallbackResponseDto.java create mode 100644 src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2FailureHandler.java create mode 100644 src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandler.java create mode 100644 src/main/java/apptive/devlog/domain/oauth2/model/OAuth2Attributes.java create mode 100644 src/main/java/apptive/devlog/domain/oauth2/service/CustomOAuth2UserService.java create mode 100644 src/main/java/apptive/devlog/domain/oauth2/service/OAuth2PkceService.java create mode 100644 src/main/java/apptive/devlog/domain/user/controller/UserController.java create mode 100644 src/main/java/apptive/devlog/domain/user/dto/UserProfileRequestDto.java create mode 100644 src/main/java/apptive/devlog/domain/user/dto/UserProfileResponseDto.java create mode 100644 src/main/java/apptive/devlog/domain/user/entity/User.java create mode 100644 src/main/java/apptive/devlog/domain/user/enums/Gender.java create mode 100644 src/main/java/apptive/devlog/domain/user/enums/Provider.java create mode 100644 src/main/java/apptive/devlog/domain/user/enums/Role.java create mode 100644 src/main/java/apptive/devlog/domain/user/repository/UserRepository.java create mode 100644 src/main/java/apptive/devlog/domain/user/service/UserService.java create mode 100644 src/main/java/apptive/devlog/global/annotation/InjectEmail.java create mode 100644 src/main/java/apptive/devlog/global/annotation/InjectToken.java create mode 100644 src/main/java/apptive/devlog/global/config/JpaConfig.java create mode 100644 src/main/java/apptive/devlog/global/config/WebConfig.java create mode 100644 src/main/java/apptive/devlog/global/resolver/InjectEmailArgumentResolver.java create mode 100644 src/main/java/apptive/devlog/global/resolver/InjectTokenArgumentResolver.java create mode 100644 src/main/java/apptive/devlog/global/response/api/ApiResponse.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/CustomException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/DuplicateEmailException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/InvalidEmailOrPasswordException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/InvalidPasswordException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/InvalidProviderException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/InvalidRefreshTokenException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/InvalidStateFormatException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/InvalidTokenException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/OAuth2TokenRequestException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/OAuth2UserInfoException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/TokenInjectionFailedException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/UnauthenticatedUserException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/exception/UserNotFoundException.java create mode 100644 src/main/java/apptive/devlog/global/response/error/handler/GlobalExceptionHandler.java create mode 100644 src/main/java/apptive/devlog/global/response/error/model/ErrorCode.java create mode 100644 src/main/java/apptive/devlog/global/response/error/model/ErrorResponse.java create mode 100644 src/main/java/apptive/devlog/global/security/config/SecurityConfig.java create mode 100644 src/main/java/apptive/devlog/global/security/filter/JwtAuthenticationFilter.java create mode 100644 src/main/java/apptive/devlog/global/security/jwt/JwtTokenProvider.java create mode 100644 src/main/java/apptive/devlog/global/security/service/CustomUserDetailsService.java create mode 100644 src/main/java/apptive/devlog/infrastructure/redis/config/RedisConfig.java create mode 100644 src/main/java/apptive/devlog/infrastructure/redis/repository/RedisRepository.java create mode 100644 src/main/java/apptive/devlog/support/testconfig/redis/RedisInitializer.java create mode 100644 src/test/java/apptive/devlog/domain/auth/controller/AuthControllerTest.java create mode 100644 src/test/java/apptive/devlog/domain/auth/service/AuthServiceTest.java create mode 100644 src/test/java/apptive/devlog/domain/oauth2/handler/OAuth2FailureHandlerTest.java create mode 100644 src/test/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandlerTest.java create mode 100644 src/test/java/apptive/devlog/domain/oauth2/service/CustomOAuth2UserServiceTest.java create mode 100644 src/test/java/apptive/devlog/domain/user/controller/UserControllerTest.java create mode 100644 src/test/java/apptive/devlog/domain/user/service/UserServiceTest.java diff --git a/build.gradle b/build.gradle index eeeb325..7a21d9f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,38 +1,55 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.4.4' - id 'io.spring.dependency-management' version '1.1.7' + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' } group = 'apptive' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'com.mysql:mysql-connector-j' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/src/main/java/apptive/devlog/common/base/BaseEntity.java b/src/main/java/apptive/devlog/common/base/BaseEntity.java new file mode 100644 index 0000000..6536fd2 --- /dev/null +++ b/src/main/java/apptive/devlog/common/base/BaseEntity.java @@ -0,0 +1,24 @@ +package apptive.devlog.common.base; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public abstract class BaseEntity { + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; +} diff --git a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java new file mode 100644 index 0000000..f3f038f --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java @@ -0,0 +1,41 @@ +package apptive.devlog.domain.auth.controller; + +import apptive.devlog.domain.auth.dto.*; +import apptive.devlog.global.annotation.InjectToken; +import apptive.devlog.domain.auth.service.AuthService; +import apptive.devlog.global.response.api.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + private final AuthService authService; + + @PostMapping("/signup") + public ResponseEntity> signup(@Valid @RequestBody UserSignupRequestDto requestDto) { + UserSignupResponseDto responseDto = authService.signup(requestDto); + return ResponseEntity.ok(ApiResponse.success(responseDto)); + } + + @PostMapping("/login") + public ResponseEntity> login(@Valid @RequestBody UserLoginRequestDto requestDto) { + UserLoginResponseDto responseDto = authService.login(requestDto); + return ResponseEntity.ok(ApiResponse.success(responseDto)); + } + + @PostMapping("/refresh") + public ResponseEntity> refresh(@Valid @InjectToken UserRefreshRequestDto requestDto) { + UserRefreshResponseDto responseDto = authService.refresh(requestDto); + return ResponseEntity.ok(ApiResponse.success(responseDto)); + } + + @PostMapping("/logout") + public ResponseEntity> logout(@Valid @InjectToken UserLogoutRequestDto requestDto) { + authService.logout(requestDto); + return ResponseEntity.ok(ApiResponse.success("logout success", null)); + } +} diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserLoginRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserLoginRequestDto.java new file mode 100644 index 0000000..3f18dd9 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserLoginRequestDto.java @@ -0,0 +1,18 @@ +package apptive.devlog.domain.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class UserLoginRequestDto { + @Email + @NotBlank + private String email; + @NotBlank + private String password; +} diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserLoginResponseDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserLoginResponseDto.java new file mode 100644 index 0000000..7bd8ccc --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserLoginResponseDto.java @@ -0,0 +1,11 @@ +package apptive.devlog.domain.auth.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UserLoginResponseDto { + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserLogoutRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserLogoutRequestDto.java new file mode 100644 index 0000000..7d7c343 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserLogoutRequestDto.java @@ -0,0 +1,14 @@ +package apptive.devlog.domain.auth.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class UserLogoutRequestDto { + @NotBlank + private String accessToken; + @NotBlank + private String refreshToken; +} diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshRequestDto.java new file mode 100644 index 0000000..ae295e7 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshRequestDto.java @@ -0,0 +1,14 @@ +package apptive.devlog.domain.auth.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class UserRefreshRequestDto { + @NotBlank + private String accessToken; + @NotBlank + private String refreshToken; +} diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshResponseDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshResponseDto.java new file mode 100644 index 0000000..06b0f1a --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshResponseDto.java @@ -0,0 +1,11 @@ +package apptive.devlog.domain.auth.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UserRefreshResponseDto { + private String accessToken; + private String refreshToken; +} \ No newline at end of file diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserSignupRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserSignupRequestDto.java new file mode 100644 index 0000000..48537fc --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserSignupRequestDto.java @@ -0,0 +1,51 @@ +package apptive.devlog.domain.auth.dto; + +import apptive.devlog.domain.user.entity.User; +import apptive.devlog.domain.user.enums.Gender; +import apptive.devlog.domain.user.enums.Provider; +import apptive.devlog.domain.user.enums.Role; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.time.LocalDate; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class UserSignupRequestDto { + @Email + @NotBlank + private String email; + + @Size(min = 8, max = 20) + @NotBlank + private String password; + + @NotBlank + private String name; + + private String nickname; + + private LocalDate birth; + + private Gender gender; + + public User toEntity(PasswordEncoder passwordEncoder) { + User user = User.builder() + .email(email) + .name(name) + .nickname(nickname) + .birth(birth) + .gender(gender) + .password(passwordEncoder.encode(password)) + .role(Role.USER) + .build(); + user.addProvider(Provider.LOCAL); + return user; + } +} diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserSignupResponseDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserSignupResponseDto.java new file mode 100644 index 0000000..35ccb06 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserSignupResponseDto.java @@ -0,0 +1,13 @@ +package apptive.devlog.domain.auth.dto; + +import apptive.devlog.domain.user.entity.User; +import lombok.Getter; + +@Getter +public class UserSignupResponseDto { + private final String email; + + public UserSignupResponseDto(User user) { + this.email = user.getEmail(); + } +} diff --git a/src/main/java/apptive/devlog/domain/auth/service/AuthService.java b/src/main/java/apptive/devlog/domain/auth/service/AuthService.java new file mode 100644 index 0000000..9e9b324 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/service/AuthService.java @@ -0,0 +1,78 @@ +package apptive.devlog.domain.auth.service; + +import apptive.devlog.domain.auth.dto.*; +import apptive.devlog.domain.user.enums.Provider; +import apptive.devlog.global.response.error.exception.DuplicateEmailException; +import apptive.devlog.global.response.error.exception.InvalidEmailOrPasswordException; +import apptive.devlog.global.response.error.exception.InvalidProviderException; +import apptive.devlog.global.response.error.exception.InvalidRefreshTokenException; +import apptive.devlog.global.security.jwt.JwtTokenProvider; +import apptive.devlog.infrastructure.redis.repository.RedisRepository; +import apptive.devlog.domain.user.entity.User; +import apptive.devlog.domain.user.repository.UserRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; + private final RedisRepository redisRepository; + + @Transactional + public UserSignupResponseDto signup(UserSignupRequestDto requestDto) { + if (userRepository.existsByEmail(requestDto.getEmail())) { + throw new DuplicateEmailException(); + } + User user = requestDto.toEntity(passwordEncoder); + userRepository.save(user); + return new UserSignupResponseDto(user); + } + + @Transactional + public UserLoginResponseDto login(UserLoginRequestDto requestDto) { + User user = userRepository.findByEmail(requestDto.getEmail()).orElseThrow(() -> new IllegalArgumentException("Invalid email or password")); + if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) { + throw new InvalidEmailOrPasswordException(); + } + if (!user.getProviders().contains(Provider.LOCAL)) { + throw new InvalidProviderException(); + } + String accessToken = jwtTokenProvider.generateAccessToken(user.getEmail()); + String refreshToken = jwtTokenProvider.generateRefreshToken(user.getEmail()); + return new UserLoginResponseDto(accessToken, refreshToken); + } + + @Transactional + public UserRefreshResponseDto refresh(UserRefreshRequestDto requestDto) { + String accessToken = requestDto.getAccessToken(); + String refreshToken = requestDto.getRefreshToken(); + + if (!jwtTokenProvider.validateRefreshToken(refreshToken)) { + throw new InvalidRefreshTokenException(); + } + + String email = jwtTokenProvider.getEmailFromToken(refreshToken); + + redisRepository.deleteAccessToken(accessToken); + redisRepository.deleteRefreshToken(refreshToken); + + String newAccessToken = jwtTokenProvider.generateAccessToken(email); + String newRefreshToken = jwtTokenProvider.generateRefreshToken(email); + + return new UserRefreshResponseDto(newAccessToken, newRefreshToken); + } + + @Transactional + public void logout(UserLogoutRequestDto requestDto) { + String accessToken = requestDto.getAccessToken(); + String refreshToken = requestDto.getRefreshToken(); + + redisRepository.deleteAccessToken(accessToken); + redisRepository.deleteRefreshToken(refreshToken); + } +} diff --git a/src/main/java/apptive/devlog/domain/oauth2/controller/OAuth2PkceController.java b/src/main/java/apptive/devlog/domain/oauth2/controller/OAuth2PkceController.java new file mode 100644 index 0000000..bdeab88 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/oauth2/controller/OAuth2PkceController.java @@ -0,0 +1,24 @@ +package apptive.devlog.domain.oauth2.controller; + +import apptive.devlog.domain.oauth2.dto.OAuth2CallbackRequestDto; +import apptive.devlog.domain.oauth2.dto.OAuth2CallbackResponseDto; +import apptive.devlog.domain.oauth2.service.OAuth2PkceService; +import apptive.devlog.global.response.api.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/oauth2") +public class OAuth2PkceController { + + private final OAuth2PkceService pkceService; + + @PostMapping("/pkce/callback") + public ResponseEntity> handleCallback(@Valid @RequestBody OAuth2CallbackRequestDto requestDto) { + OAuth2CallbackResponseDto responseDto = pkceService.handleCallback(requestDto); + return ResponseEntity.ok(ApiResponse.success(responseDto)); + } +} diff --git a/src/main/java/apptive/devlog/domain/oauth2/dto/OAuth2CallbackRequestDto.java b/src/main/java/apptive/devlog/domain/oauth2/dto/OAuth2CallbackRequestDto.java new file mode 100644 index 0000000..ad1814a --- /dev/null +++ b/src/main/java/apptive/devlog/domain/oauth2/dto/OAuth2CallbackRequestDto.java @@ -0,0 +1,12 @@ +package apptive.devlog.domain.oauth2.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class OAuth2CallbackRequestDto { + @NotBlank + private String code; + @NotBlank + private String state; +} diff --git a/src/main/java/apptive/devlog/domain/oauth2/dto/OAuth2CallbackResponseDto.java b/src/main/java/apptive/devlog/domain/oauth2/dto/OAuth2CallbackResponseDto.java new file mode 100644 index 0000000..ff9179e --- /dev/null +++ b/src/main/java/apptive/devlog/domain/oauth2/dto/OAuth2CallbackResponseDto.java @@ -0,0 +1,11 @@ +package apptive.devlog.domain.oauth2.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class OAuth2CallbackResponseDto { + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2FailureHandler.java b/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2FailureHandler.java new file mode 100644 index 0000000..e58b063 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2FailureHandler.java @@ -0,0 +1,34 @@ +package apptive.devlog.domain.oauth2.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class OAuth2FailureHandler implements AuthenticationFailureHandler { + private final ObjectMapper objectMapper; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { + Map errorResponse = new HashMap<>(); + errorResponse.put("success", false); + errorResponse.put("error", "OAuth2 인증 실패"); + errorResponse.put("message", exception.getMessage()); + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + String responseBody = objectMapper.writeValueAsString(errorResponse); + response.getWriter().write(responseBody); + } +} diff --git a/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandler.java b/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandler.java new file mode 100644 index 0000000..f11cdd2 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandler.java @@ -0,0 +1,40 @@ +package apptive.devlog.domain.oauth2.handler; + +import apptive.devlog.global.security.jwt.JwtTokenProvider; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@RequiredArgsConstructor +@Configuration +public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + private final JwtTokenProvider jwtTokenProvider; + private final ObjectMapper objectMapper; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.Authentication authentication) throws IOException { + OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); + String email = oAuth2User.getAttribute("email"); + + String accessToken = jwtTokenProvider.generateAccessToken(email); + String refreshToken = jwtTokenProvider.generateRefreshToken(email); + + Map tokens = new HashMap<>(); + tokens.put("accessToken", accessToken); + tokens.put("refreshToken", refreshToken); + + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + String responseBody = objectMapper.writeValueAsString(tokens); + response.getWriter().write(responseBody); + } +} diff --git a/src/main/java/apptive/devlog/domain/oauth2/model/OAuth2Attributes.java b/src/main/java/apptive/devlog/domain/oauth2/model/OAuth2Attributes.java new file mode 100644 index 0000000..a0da543 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/oauth2/model/OAuth2Attributes.java @@ -0,0 +1,81 @@ +package apptive.devlog.domain.oauth2.model; + +import apptive.devlog.domain.user.entity.User; +import apptive.devlog.domain.user.enums.Provider; +import apptive.devlog.domain.user.enums.Role; +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +@Getter +public class OAuth2Attributes { + + private final String nameAttributeKey; + private final Provider provider; + private final Map attributes; + private final String email; + private final String name; + + public OAuth2Attributes(String nameAttributeKey, Provider provider, + Map attributes, String email, String name) { + this.nameAttributeKey = nameAttributeKey; + this.provider = provider; + // this.attributes = attributes; + this.email = email; + this.name = name; + this.attributes = new HashMap<>(attributes); // 임시 방편 + this.attributes.put("email", email); // 임시 방편 + } + + public static OAuth2Attributes of(String registrationId, String userNameAttributeName, + Map attributes) { + if ("kakao".equals(registrationId)) { + return ofKakao(attributes); + } else if ("naver".equals(registrationId)) { + return ofNaver(attributes); + } else { + return ofGoogle(userNameAttributeName, attributes); + } + } + + private static OAuth2Attributes ofGoogle(String userNameAttributeName, Map attributes) { + return new OAuth2Attributes( + userNameAttributeName, + Provider.GOOGLE, + attributes, + (String) attributes.get("email"), + (String) attributes.get("name") + ); + } + + private static OAuth2Attributes ofNaver(Map attributes) { + Map response = (Map) attributes.get("response"); + + return new OAuth2Attributes( + "id", + Provider.NAVER, + response, + (String) response.get("email"), + (String) response.get("name") + ); + } + + private static OAuth2Attributes ofKakao(Map attributes) { + Long id = ((Number) attributes.get("id")).longValue(); + Map kakaoAccount = (Map) attributes.get("kakao_account"); + Map profile = (Map) kakaoAccount.get("profile"); + + return new OAuth2Attributes( + "id", + Provider.KAKAO, + attributes, + (String) kakaoAccount.get("email"), + (String) profile.get("nickname") + ); + } + + public User toEntity() { + return User.of(email, name, provider, Role.USER); + } +} diff --git a/src/main/java/apptive/devlog/domain/oauth2/service/CustomOAuth2UserService.java b/src/main/java/apptive/devlog/domain/oauth2/service/CustomOAuth2UserService.java new file mode 100644 index 0000000..8c825f4 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/oauth2/service/CustomOAuth2UserService.java @@ -0,0 +1,42 @@ +package apptive.devlog.domain.oauth2.service; + +import apptive.devlog.domain.oauth2.model.OAuth2Attributes; +import apptive.devlog.domain.user.entity.User; +import apptive.devlog.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +@RequiredArgsConstructor +@Service +public class CustomOAuth2UserService implements OAuth2UserService { + private final UserRepository userRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + OAuth2User oAuth2User = new DefaultOAuth2UserService().loadUser(userRequest); + + OAuth2Attributes oAuth2Attributes = OAuth2Attributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); + User user = saveOrUpdate(oAuth2Attributes); + + return new DefaultOAuth2User( + Collections.singleton(new SimpleGrantedAuthority(user.getRole().name())), + oAuth2Attributes.getAttributes(), + oAuth2Attributes.getNameAttributeKey() + ); + } + + private User saveOrUpdate(OAuth2Attributes attributes) { + return userRepository.findByEmail(attributes.getEmail()).orElseGet(() -> userRepository.save(attributes.toEntity())); + } +} diff --git a/src/main/java/apptive/devlog/domain/oauth2/service/OAuth2PkceService.java b/src/main/java/apptive/devlog/domain/oauth2/service/OAuth2PkceService.java new file mode 100644 index 0000000..815c4bb --- /dev/null +++ b/src/main/java/apptive/devlog/domain/oauth2/service/OAuth2PkceService.java @@ -0,0 +1,100 @@ +package apptive.devlog.domain.oauth2.service; + +import apptive.devlog.domain.oauth2.dto.OAuth2CallbackRequestDto; +import apptive.devlog.domain.oauth2.dto.OAuth2CallbackResponseDto; +import apptive.devlog.global.response.error.exception.InvalidStateFormatException; +import apptive.devlog.global.response.error.exception.OAuth2TokenRequestException; +import apptive.devlog.global.response.error.exception.OAuth2UserInfoException; +import apptive.devlog.global.security.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import java.util.Map; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class OAuth2PkceService { + + private final JwtTokenProvider jwtTokenProvider; + private final WebClient webClient; + + @Value("${spring.security.oauth2.client.registration.google.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.registration.google.client-secret}") + private String clientSecret; + + @Value("${spring.security.oauth2.client.registration.google.redirect-uri}") + private String redirectUri; + + public OAuth2CallbackResponseDto handleCallback(OAuth2CallbackRequestDto requestDto) { + String code = requestDto.getCode(); + String state = requestDto.getState(); + + String[] parts = state != null ? state.split("::") : new String[0]; + if (parts.length != 2) { + throw new InvalidStateFormatException(); + } + String deviceId = parts[0]; + String codeVerifier = parts[1]; + + Map tokenResponse = requestAccessToken(code, codeVerifier); + String googleAccessToken = Optional.ofNullable(tokenResponse.get("access_token")) + .map(Object::toString) + .orElseThrow(() -> new OAuth2TokenRequestException("Access token not found")); + + Map userInfo = fetchGoogleUserInfo(googleAccessToken); + String email = Optional.ofNullable(userInfo.get("email")) + .map(Object::toString) + .orElseThrow(() -> new OAuth2UserInfoException("Email not found in user info")); + + String accessToken = jwtTokenProvider.generateAccessToken(email); + String refreshToken = jwtTokenProvider.generateRefreshToken(email); + + return new OAuth2CallbackResponseDto(accessToken, refreshToken); + } + + private Map requestAccessToken(String code, String codeVerifier) { + MultiValueMap tokenRequest = new LinkedMultiValueMap<>(); + tokenRequest.add("client_id", clientId); + tokenRequest.add("client_secret", clientSecret); + tokenRequest.add("grant_type", "authorization_code"); + tokenRequest.add("code", code); + tokenRequest.add("redirect_uri", redirectUri); + tokenRequest.add("code_verifier", codeVerifier); + + try { + return webClient.post() + .uri("https://oauth2.googleapis.com/token") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .bodyValue(tokenRequest) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .block(); + } catch (WebClientResponseException ex) { + throw new OAuth2TokenRequestException("Failed to get token from Google: " + ex.getResponseBodyAsString()); + } + } + + private Map fetchGoogleUserInfo(String accessToken) { + try { + return webClient.get() + .uri("https://www.googleapis.com/oauth2/v3/userinfo") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .block(); + } catch (WebClientResponseException ex) { + throw new OAuth2UserInfoException("Failed to get user info from Google: " + ex.getResponseBodyAsString()); + } + } +} diff --git a/src/main/java/apptive/devlog/domain/user/controller/UserController.java b/src/main/java/apptive/devlog/domain/user/controller/UserController.java new file mode 100644 index 0000000..492ce0c --- /dev/null +++ b/src/main/java/apptive/devlog/domain/user/controller/UserController.java @@ -0,0 +1,25 @@ +package apptive.devlog.domain.user.controller; + +import apptive.devlog.global.annotation.InjectEmail; +import apptive.devlog.domain.user.dto.UserProfileRequestDto; +import apptive.devlog.domain.user.dto.UserProfileResponseDto; +import apptive.devlog.domain.user.service.UserService; +import apptive.devlog.global.response.api.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +public class UserController { + private final UserService userService; + + @GetMapping("/profile") + public ResponseEntity> getUserProfile(@Valid @InjectEmail UserProfileRequestDto requestDto) { + return ResponseEntity.ok(ApiResponse.success(userService.getUserProfile(requestDto))); + } +} diff --git a/src/main/java/apptive/devlog/domain/user/dto/UserProfileRequestDto.java b/src/main/java/apptive/devlog/domain/user/dto/UserProfileRequestDto.java new file mode 100644 index 0000000..111c5ee --- /dev/null +++ b/src/main/java/apptive/devlog/domain/user/dto/UserProfileRequestDto.java @@ -0,0 +1,12 @@ +package apptive.devlog.domain.user.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class UserProfileRequestDto { + @NotBlank + private String email; +} diff --git a/src/main/java/apptive/devlog/domain/user/dto/UserProfileResponseDto.java b/src/main/java/apptive/devlog/domain/user/dto/UserProfileResponseDto.java new file mode 100644 index 0000000..196040f --- /dev/null +++ b/src/main/java/apptive/devlog/domain/user/dto/UserProfileResponseDto.java @@ -0,0 +1,24 @@ +package apptive.devlog.domain.user.dto; + +import apptive.devlog.domain.user.entity.User; +import apptive.devlog.domain.user.enums.Gender; +import lombok.Getter; + +import java.time.LocalDate; + +@Getter +public class UserProfileResponseDto { + private final String email; + private final String nickname; + private final String name; + private final LocalDate birthday; + private final Gender gender; + + public UserProfileResponseDto(User user) { + this.email = user.getEmail(); + this.nickname = user.getNickname(); + this.name = user.getName(); + this.birthday = user.getBirth(); + this.gender = user.getGender(); + } +} diff --git a/src/main/java/apptive/devlog/domain/user/entity/User.java b/src/main/java/apptive/devlog/domain/user/entity/User.java new file mode 100644 index 0000000..548c665 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/user/entity/User.java @@ -0,0 +1,68 @@ +package apptive.devlog.domain.user.entity; + +import apptive.devlog.common.base.BaseEntity; +import apptive.devlog.domain.user.enums.Gender; +import apptive.devlog.domain.user.enums.Provider; +import apptive.devlog.domain.user.enums.Role; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDate; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Table(name = "users") +public class User extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String email; + + private String nickname; + + @Column(nullable = false) + private String name; + + private LocalDate birth; + + @Enumerated(EnumType.STRING) + private Gender gender; + + private String password; + + private String providerId; + + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "user_providers", joinColumns = @JoinColumn(name = "user_id")) + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Set providers; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; + + public User(String email, String name, Provider provider, Role role) { + this.email = email; + this.name = name; + this.role = role; + this.providers = new HashSet<>(Collections.singletonList(provider)); + } + + public void addProvider(Provider provider) { + if (providers == null) { this.providers = new HashSet<>(); } + this.providers.add(provider); + } + + public static User of(String email, String name, Provider provider, Role role) { + return new User(email, name, provider, role); + } +} diff --git a/src/main/java/apptive/devlog/domain/user/enums/Gender.java b/src/main/java/apptive/devlog/domain/user/enums/Gender.java new file mode 100644 index 0000000..9191dcf --- /dev/null +++ b/src/main/java/apptive/devlog/domain/user/enums/Gender.java @@ -0,0 +1,5 @@ +package apptive.devlog.domain.user.enums; + +public enum Gender { + MALE, FEMALE, OTHER +} diff --git a/src/main/java/apptive/devlog/domain/user/enums/Provider.java b/src/main/java/apptive/devlog/domain/user/enums/Provider.java new file mode 100644 index 0000000..95307f0 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/user/enums/Provider.java @@ -0,0 +1,5 @@ +package apptive.devlog.domain.user.enums; + +public enum Provider { + LOCAL, GOOGLE, KAKAO, NAVER +} diff --git a/src/main/java/apptive/devlog/domain/user/enums/Role.java b/src/main/java/apptive/devlog/domain/user/enums/Role.java new file mode 100644 index 0000000..84fa108 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/user/enums/Role.java @@ -0,0 +1,5 @@ +package apptive.devlog.domain.user.enums; + +public enum Role { + USER +} diff --git a/src/main/java/apptive/devlog/domain/user/repository/UserRepository.java b/src/main/java/apptive/devlog/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..bddced6 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/user/repository/UserRepository.java @@ -0,0 +1,12 @@ +package apptive.devlog.domain.user.repository; + +import apptive.devlog.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + boolean existsByEmail(String email); + boolean existsByNickname(String nickname); +} diff --git a/src/main/java/apptive/devlog/domain/user/service/UserService.java b/src/main/java/apptive/devlog/domain/user/service/UserService.java new file mode 100644 index 0000000..7517ff2 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/user/service/UserService.java @@ -0,0 +1,21 @@ +package apptive.devlog.domain.user.service; + +import apptive.devlog.domain.user.dto.UserProfileRequestDto; +import apptive.devlog.domain.user.dto.UserProfileResponseDto; +import apptive.devlog.domain.user.entity.User; +import apptive.devlog.domain.user.repository.UserRepository; +import apptive.devlog.global.response.error.exception.UserNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserService { + private final UserRepository userRepository; + + public UserProfileResponseDto getUserProfile(UserProfileRequestDto requestDto) { + User user = userRepository.findByEmail(requestDto.getEmail()) + .orElseThrow(UserNotFoundException::new); + return new UserProfileResponseDto(user); + } +} diff --git a/src/main/java/apptive/devlog/global/annotation/InjectEmail.java b/src/main/java/apptive/devlog/global/annotation/InjectEmail.java new file mode 100644 index 0000000..7aace98 --- /dev/null +++ b/src/main/java/apptive/devlog/global/annotation/InjectEmail.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.annotation; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface InjectEmail { +} diff --git a/src/main/java/apptive/devlog/global/annotation/InjectToken.java b/src/main/java/apptive/devlog/global/annotation/InjectToken.java new file mode 100644 index 0000000..1260b94 --- /dev/null +++ b/src/main/java/apptive/devlog/global/annotation/InjectToken.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.annotation; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface InjectToken { +} diff --git a/src/main/java/apptive/devlog/global/config/JpaConfig.java b/src/main/java/apptive/devlog/global/config/JpaConfig.java new file mode 100644 index 0000000..7acd138 --- /dev/null +++ b/src/main/java/apptive/devlog/global/config/JpaConfig.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaConfig { +} diff --git a/src/main/java/apptive/devlog/global/config/WebConfig.java b/src/main/java/apptive/devlog/global/config/WebConfig.java new file mode 100644 index 0000000..777c843 --- /dev/null +++ b/src/main/java/apptive/devlog/global/config/WebConfig.java @@ -0,0 +1,30 @@ +package apptive.devlog.global.config; + +import apptive.devlog.global.resolver.InjectTokenArgumentResolver; +import apptive.devlog.global.resolver.InjectEmailArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@RequiredArgsConstructor +@Configuration +public class WebConfig implements WebMvcConfigurer { + private final InjectEmailArgumentResolver injectEmailArgumentResolver; + private final InjectTokenArgumentResolver injectTokenArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(injectEmailArgumentResolver); + resolvers.add(injectTokenArgumentResolver); + } + + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/src/main/java/apptive/devlog/global/resolver/InjectEmailArgumentResolver.java b/src/main/java/apptive/devlog/global/resolver/InjectEmailArgumentResolver.java new file mode 100644 index 0000000..fcf8b7b --- /dev/null +++ b/src/main/java/apptive/devlog/global/resolver/InjectEmailArgumentResolver.java @@ -0,0 +1,77 @@ +package apptive.devlog.global.resolver; + +import apptive.devlog.global.annotation.InjectEmail; +import apptive.devlog.global.response.error.exception.TokenInjectionFailedException; +import apptive.devlog.global.response.error.exception.UnauthenticatedUserException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import java.lang.reflect.Field; + +@Slf4j +@Component +@RequiredArgsConstructor +public class InjectEmailArgumentResolver implements HandlerMethodArgumentResolver { + + private final ObjectMapper objectMapper; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(InjectEmail.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + + String email = getCurrentUserEmail(); + + Class parameterType = parameter.getParameterType(); + Object dto; + + if ("POST".equalsIgnoreCase(request.getMethod()) || "PUT".equalsIgnoreCase(request.getMethod())) { + dto = objectMapper.readValue(request.getInputStream(), parameterType); + } else { + dto = parameterType.getDeclaredConstructor().newInstance(); + } + + injectIfFieldExists(dto, "email", email); + + return dto; + } + + private String getCurrentUserEmail() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + throw new UnauthenticatedUserException(); + } + return authentication.getName(); + } + + private void injectIfFieldExists(Object target, String fieldName, String value) { + try { + Field field = target.getClass().getDeclaredField(fieldName); + if (field.getType().equals(String.class)) { + field.setAccessible(true); + field.set(target, value); + } + } catch (NoSuchFieldException ignored) { + } catch (IllegalAccessException e) { + throw new TokenInjectionFailedException(fieldName, e); + } + } +} diff --git a/src/main/java/apptive/devlog/global/resolver/InjectTokenArgumentResolver.java b/src/main/java/apptive/devlog/global/resolver/InjectTokenArgumentResolver.java new file mode 100644 index 0000000..9000441 --- /dev/null +++ b/src/main/java/apptive/devlog/global/resolver/InjectTokenArgumentResolver.java @@ -0,0 +1,66 @@ +package apptive.devlog.global.resolver; + +import apptive.devlog.global.annotation.InjectToken; +import apptive.devlog.global.response.error.exception.InvalidTokenException; +import apptive.devlog.global.response.error.exception.TokenInjectionFailedException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import java.lang.reflect.Field; + +@Slf4j +@Component +@RequiredArgsConstructor +@Order(0) +public class InjectTokenArgumentResolver implements HandlerMethodArgumentResolver { + + private final ObjectMapper objectMapper; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(InjectToken.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + + Object dto = objectMapper.readValue(request.getInputStream(), parameter.getParameterType()); + + String accessToken = request.getHeader("Authorization"); + if (accessToken != null && accessToken.startsWith("Bearer ")) { + accessToken = accessToken.substring(7); + } else { + throw new InvalidTokenException(); + } + + injectIfFieldExists(dto, "accessToken", accessToken); + + return dto; + } + + private void injectIfFieldExists(Object target, String fieldName, String value) { + try { + Field field = target.getClass().getDeclaredField(fieldName); + if (field.getType().equals(String.class)) { + field.setAccessible(true); + field.set(target, value); + } + } catch (NoSuchFieldException ignored) { + } catch (IllegalAccessException e) { + throw new TokenInjectionFailedException(fieldName, e); + } + } +} diff --git a/src/main/java/apptive/devlog/global/response/api/ApiResponse.java b/src/main/java/apptive/devlog/global/response/api/ApiResponse.java new file mode 100644 index 0000000..ca3e052 --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/api/ApiResponse.java @@ -0,0 +1,21 @@ +package apptive.devlog.global.response.api; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ApiResponse { + private final int status; + private final String message; + private final T data; + + public static ApiResponse success(T data) { + return new ApiResponse<>(200, "요청이 성공했습니다.", data); + } + + public static ApiResponse success(String message, T data) { + return new ApiResponse<>(200, message, data); + } +} + diff --git a/src/main/java/apptive/devlog/global/response/error/exception/CustomException.java b/src/main/java/apptive/devlog/global/response/error/exception/CustomException.java new file mode 100644 index 0000000..eec84ad --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/CustomException.java @@ -0,0 +1,21 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public abstract class CustomException extends RuntimeException { + private final ErrorCode errorCode; + + public CustomException(ErrorCode errorCode, Throwable cause) { + super(errorCode.getMessage(), cause); + this.errorCode = errorCode; + } + + protected CustomException(ErrorCode errorCode, String customMessage) { + super(customMessage); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/exception/DuplicateEmailException.java b/src/main/java/apptive/devlog/global/response/error/exception/DuplicateEmailException.java new file mode 100644 index 0000000..a75d1b8 --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/DuplicateEmailException.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; + +public class DuplicateEmailException extends CustomException { + public DuplicateEmailException() { + super(ErrorCode.DUPLICATE_EMAIL); + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/exception/InvalidEmailOrPasswordException.java b/src/main/java/apptive/devlog/global/response/error/exception/InvalidEmailOrPasswordException.java new file mode 100644 index 0000000..959ee2e --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/InvalidEmailOrPasswordException.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; + +public class InvalidEmailOrPasswordException extends CustomException { + public InvalidEmailOrPasswordException() { + super(ErrorCode.INVALID_EMAIL_OR_PASSWORD); + } +} \ No newline at end of file diff --git a/src/main/java/apptive/devlog/global/response/error/exception/InvalidPasswordException.java b/src/main/java/apptive/devlog/global/response/error/exception/InvalidPasswordException.java new file mode 100644 index 0000000..8391727 --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/InvalidPasswordException.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; + +public class InvalidPasswordException extends CustomException { + public InvalidPasswordException() { + super(ErrorCode.INVALID_PASSWORD); + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/exception/InvalidProviderException.java b/src/main/java/apptive/devlog/global/response/error/exception/InvalidProviderException.java new file mode 100644 index 0000000..9d9c490 --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/InvalidProviderException.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; + +public class InvalidProviderException extends CustomException { + public InvalidProviderException() { + super(ErrorCode.INVALID_PROVIDER); + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/exception/InvalidRefreshTokenException.java b/src/main/java/apptive/devlog/global/response/error/exception/InvalidRefreshTokenException.java new file mode 100644 index 0000000..a59d910 --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/InvalidRefreshTokenException.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; + +public class InvalidRefreshTokenException extends CustomException { + public InvalidRefreshTokenException() { + super(ErrorCode.INVALID_REFRESH_TOKEN); + } +} \ No newline at end of file diff --git a/src/main/java/apptive/devlog/global/response/error/exception/InvalidStateFormatException.java b/src/main/java/apptive/devlog/global/response/error/exception/InvalidStateFormatException.java new file mode 100644 index 0000000..a15c2dc --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/InvalidStateFormatException.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; + +public class InvalidStateFormatException extends CustomException { + public InvalidStateFormatException() { + super(ErrorCode.INVALID_STATE_FORMAT); + } +} \ No newline at end of file diff --git a/src/main/java/apptive/devlog/global/response/error/exception/InvalidTokenException.java b/src/main/java/apptive/devlog/global/response/error/exception/InvalidTokenException.java new file mode 100644 index 0000000..392c764 --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/InvalidTokenException.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; + +public class InvalidTokenException extends CustomException { + public InvalidTokenException() { + super(ErrorCode.INVALID_TOKEN); + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/exception/OAuth2TokenRequestException.java b/src/main/java/apptive/devlog/global/response/error/exception/OAuth2TokenRequestException.java new file mode 100644 index 0000000..3feda0a --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/OAuth2TokenRequestException.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; + +public class OAuth2TokenRequestException extends CustomException { + public OAuth2TokenRequestException(String message) { + super(ErrorCode.OAUTH2_TOKEN_REQUEST_FAILED, message); + } +} \ No newline at end of file diff --git a/src/main/java/apptive/devlog/global/response/error/exception/OAuth2UserInfoException.java b/src/main/java/apptive/devlog/global/response/error/exception/OAuth2UserInfoException.java new file mode 100644 index 0000000..507113b --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/OAuth2UserInfoException.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; + +public class OAuth2UserInfoException extends CustomException { + public OAuth2UserInfoException(String message) { + super(ErrorCode.OAUTH2_USERINFO_FAILED, message); + } +} \ No newline at end of file diff --git a/src/main/java/apptive/devlog/global/response/error/exception/TokenInjectionFailedException.java b/src/main/java/apptive/devlog/global/response/error/exception/TokenInjectionFailedException.java new file mode 100644 index 0000000..9812068 --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/TokenInjectionFailedException.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; + +public class TokenInjectionFailedException extends CustomException { + public TokenInjectionFailedException(String fieldName, Throwable cause) { + super(ErrorCode.TOKEN_INJECTION_FAILED, cause); + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/exception/UnauthenticatedUserException.java b/src/main/java/apptive/devlog/global/response/error/exception/UnauthenticatedUserException.java new file mode 100644 index 0000000..b9f6ee3 --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/UnauthenticatedUserException.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; + +public class UnauthenticatedUserException extends CustomException { + public UnauthenticatedUserException() { + super(ErrorCode.UNAUTHENTICATED_USER); + } +} \ No newline at end of file diff --git a/src/main/java/apptive/devlog/global/response/error/exception/UserNotFoundException.java b/src/main/java/apptive/devlog/global/response/error/exception/UserNotFoundException.java new file mode 100644 index 0000000..6586f3c --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/exception/UserNotFoundException.java @@ -0,0 +1,9 @@ +package apptive.devlog.global.response.error.exception; + +import apptive.devlog.global.response.error.model.ErrorCode; + +public class UserNotFoundException extends CustomException { + public UserNotFoundException() { + super(ErrorCode.USER_NOT_FOUND); + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/handler/GlobalExceptionHandler.java b/src/main/java/apptive/devlog/global/response/error/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..1e23abf --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/handler/GlobalExceptionHandler.java @@ -0,0 +1,72 @@ +package apptive.devlog.global.response.error.handler; + +import apptive.devlog.global.response.error.exception.*; +import apptive.devlog.global.response.error.model.ErrorCode; +import apptive.devlog.global.response.error.model.ErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private ResponseEntity buildResponse(ErrorCode errorCode) { + return ResponseEntity + .status(errorCode.getStatus()) + .body(new ErrorResponse( + errorCode.getStatus(), + errorCode.getMessage(), + errorCode.getCode())); + } + + @ExceptionHandler(DuplicateEmailException.class) + public ResponseEntity handleDuplicateEmail(DuplicateEmailException ex) { + return buildResponse(ErrorCode.DUPLICATE_EMAIL); + } + + @ExceptionHandler(InvalidPasswordException.class) + public ResponseEntity handleInvalidPassword(InvalidPasswordException ex) { + return buildResponse(ErrorCode.INVALID_PASSWORD); + } + + @ExceptionHandler(InvalidProviderException.class) + public ResponseEntity handleInvalidProvider(InvalidProviderException ex) { + return buildResponse(ErrorCode.INVALID_PROVIDER); + } + + @ExceptionHandler(InvalidStateFormatException.class) + public ResponseEntity handleInvalidState(InvalidStateFormatException ex) { + return buildResponse(ErrorCode.INVALID_STATE_FORMAT); + } + + @ExceptionHandler(InvalidRefreshTokenException.class) + public ResponseEntity handleInvalidRefreshToken(InvalidRefreshTokenException ex) { + return buildResponse(ErrorCode.INVALID_REFRESH_TOKEN); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgument(IllegalArgumentException ex) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + ex.getMessage(), + "BAD_REQUEST")); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException ex) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + ex.getMessage(), + "INTERNAL_ERROR")); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGeneral(Exception ex) { + return buildResponse(ErrorCode.UNKNOWN_ERROR); + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/model/ErrorCode.java b/src/main/java/apptive/devlog/global/response/error/model/ErrorCode.java new file mode 100644 index 0000000..e8d1d99 --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/model/ErrorCode.java @@ -0,0 +1,29 @@ +package apptive.devlog.global.response.error.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum ErrorCode { + USER_NOT_FOUND(404, "USER_001", "해당 사용자를 찾을 수 없습니다."), + DUPLICATE_EMAIL(400, "USER_002", "이미 존재하는 이메일입니다."), + INVALID_PROVIDER(400, "USER_003", "유효하지 않은 OAuth Provider입니다."), + INVALID_PASSWORD(401, "USER_004", "비밀번호가 일치하지 않습니다."), + UNAUTHENTICATED_USER(401, "USER_005", "사용자 인증 정보가 없습니다."), + INVALID_EMAIL_OR_PASSWORD(401, "USER_006", "이메일 또는 비밀번호가 잘못되었습니다."), + + INVALID_STATE_FORMAT(400, "PKCE_001", "State 파라미터 형식이 올바르지 않습니다."), + OAUTH2_TOKEN_REQUEST_FAILED(500, "PKCE_002", "OAuth2 토큰 요청에 실패했습니다."), + OAUTH2_USERINFO_FAILED(500, "PKCE_003", "OAuth2 사용자 정보 요청에 실패했습니다."), + + INVALID_TOKEN(401, "TOKEN_001", "유효하지 않은 토큰입니다."), + TOKEN_INJECTION_FAILED(500, "TOKEN_002", "토큰 주입에 실패했습니다."), + INVALID_REFRESH_TOKEN(401, "TOKEN_003", "유효하지 않은 리프레시 토큰입니다."), + + UNKNOWN_ERROR(500, "ERROR_001", "서버 내부 오류가 발생했습니다."); + + private final int status; + private final String code; + private final String message; +} diff --git a/src/main/java/apptive/devlog/global/response/error/model/ErrorResponse.java b/src/main/java/apptive/devlog/global/response/error/model/ErrorResponse.java new file mode 100644 index 0000000..ffb7dd6 --- /dev/null +++ b/src/main/java/apptive/devlog/global/response/error/model/ErrorResponse.java @@ -0,0 +1,12 @@ +package apptive.devlog.global.response.error.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ErrorResponse { + private int status; + private String message; + private String code; +} \ No newline at end of file diff --git a/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java b/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java new file mode 100644 index 0000000..d13ff07 --- /dev/null +++ b/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java @@ -0,0 +1,91 @@ +package apptive.devlog.global.security.config; + +import apptive.devlog.global.security.filter.JwtAuthenticationFilter; +import apptive.devlog.domain.oauth2.handler.OAuth2FailureHandler; +import apptive.devlog.domain.oauth2.handler.OAuth2SuccessHandler; +import apptive.devlog.domain.oauth2.service.CustomOAuth2UserService; +import apptive.devlog.global.security.service.CustomUserDetailsService; +import apptive.devlog.global.security.jwt.JwtTokenProvider; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final JwtTokenProvider jwtTokenProvider; + private final CustomUserDetailsService customUserDetailsService; + private final HttpSession httpSession; + // private final CustomOAuth2AuthorizationRequestResolver customOAuth2AuthorizationRequestResolver; + // private final CustomAuthorizationCodeTokenResponseClient customAuthorizationCodeTokenResponseClient; + private final CustomOAuth2UserService customOAuth2UserService; + private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final OAuth2FailureHandler oAuth2FailureHandler; + + @Bean + public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { + DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); + authenticationProvider.setUserDetailsService(userDetailsService); + authenticationProvider.setPasswordEncoder(passwordEncoder); + + return new ProviderManager(authenticationProvider); + } + + @Bean + public JwtAuthenticationFilter jwtAuthenticationFilter() { + return new JwtAuthenticationFilter(jwtTokenProvider, customUserDetailsService, httpSession); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/", "/auth/signup", "/auth/login", "/auth/refresh").permitAll() + .requestMatchers("/oauth2/**").permitAll() + .requestMatchers("/auth/logout", "/user/profile").hasRole("USER") + .anyRequest().authenticated() + ) + .oauth2Login(oauth2 -> oauth2 + // .loginPage("/login") + .authorizationEndpoint(endpoint -> endpoint.baseUri("/oauth2/authorization")/* .authorizationRequestResolver(customOAuth2AuthorizationRequestResolver) */) + .redirectionEndpoint(endpoint -> endpoint.baseUri("/oauth2/callback/*")) + // .tokenEndpoint().accessTokenResponseClient(customAuthorizationCodeTokenResponseClient) + .userInfoEndpoint(endpoint -> endpoint.userService(customOAuth2UserService)) + .successHandler(oAuth2SuccessHandler) + .failureHandler(oAuth2FailureHandler) + ) + .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) + .build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} + +/* + * http://localhost:8080/oauth2/authorization/google + * https://accounts.google.com/o/oauth2/v2/auth + * http://localhost:8080/login/oauth2/code/google + * https://oauth2.googleapis.com/token + * https://www.googleapis.com/oauth2/v3/userinfo + * CustomOAuth2UserService + * OAuth2AuthenticationSuccessHandler + */ diff --git a/src/main/java/apptive/devlog/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/apptive/devlog/global/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..53dfdc6 --- /dev/null +++ b/src/main/java/apptive/devlog/global/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,44 @@ +package apptive.devlog.global.security.filter; + +import apptive.devlog.global.security.service.CustomUserDetailsService; +import apptive.devlog.global.security.jwt.JwtTokenProvider; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtTokenProvider jwtTokenProvider; + private final CustomUserDetailsService customUserDetailsService; + private final HttpSession httpSession; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String token = resolveToken(request); + if (token != null && jwtTokenProvider.validateAccessToken(token)) { + String email = jwtTokenProvider.getEmailFromToken(token); + UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + httpSession.setAttribute("email", email); + } + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/apptive/devlog/global/security/jwt/JwtTokenProvider.java b/src/main/java/apptive/devlog/global/security/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..5db3cb1 --- /dev/null +++ b/src/main/java/apptive/devlog/global/security/jwt/JwtTokenProvider.java @@ -0,0 +1,75 @@ +package apptive.devlog.global.security.jwt; + +import apptive.devlog.infrastructure.redis.repository.RedisRepository; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.security.Key; + +@Slf4j +@Component +public class JwtTokenProvider { + private final Key key; + private final long accessTokenExpiration; + private final long refreshTokenExpiration; + private final RedisRepository redisRepository; + + public JwtTokenProvider( + @Value("${jwt.secret}") String secretKey, + @Value("${jwt.access-token-expiration}") long accessTokenExpiration, + @Value("${jwt.refresh-token-expiration}") long refreshTokenExpiration, + RedisRepository redisRepository) { + this.key = Keys.hmacShaKeyFor(secretKey.getBytes()); + this.accessTokenExpiration = accessTokenExpiration; + this.refreshTokenExpiration = refreshTokenExpiration; + this.redisRepository = redisRepository; + } + + public String generateToken(String email, long expiration) { + return Jwts.builder() + .setSubject(email) + .setIssuedAt(java.util.Date.from(java.time.Instant.now())) + .setExpiration(java.util.Date.from(java.time.Instant.now().plusMillis(expiration))) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + public String generateAccessToken(String email) { + String accessToken = generateToken(email, accessTokenExpiration); + redisRepository.saveAccessToken(accessToken, email); + return accessToken; + } + + public String generateRefreshToken(String email) { + String refreshToken = generateToken(email, refreshTokenExpiration); + redisRepository.saveRefreshToken(refreshToken, email); + return refreshToken; + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } + + public boolean validateAccessToken(String token) { + if (!validateToken(token)) return false; + return redisRepository.hasAccessToken(token); + } + + public boolean validateRefreshToken(String token) { + if (!validateToken(token)) return false; + return redisRepository.hasRefreshToken(token); + } + + public String getEmailFromToken(String token) { + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getSubject(); + } +} diff --git a/src/main/java/apptive/devlog/global/security/service/CustomUserDetailsService.java b/src/main/java/apptive/devlog/global/security/service/CustomUserDetailsService.java new file mode 100644 index 0000000..b3bd985 --- /dev/null +++ b/src/main/java/apptive/devlog/global/security/service/CustomUserDetailsService.java @@ -0,0 +1,21 @@ +package apptive.devlog.global.security.service; + +import apptive.devlog.domain.user.entity.User; +import apptive.devlog.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); + return org.springframework.security.core.userdetails.User.withUsername(user.getEmail()).password(user.getPassword()).roles(user.getRole().name()).build(); + } +} diff --git a/src/main/java/apptive/devlog/infrastructure/redis/config/RedisConfig.java b/src/main/java/apptive/devlog/infrastructure/redis/config/RedisConfig.java new file mode 100644 index 0000000..597b65c --- /dev/null +++ b/src/main/java/apptive/devlog/infrastructure/redis/config/RedisConfig.java @@ -0,0 +1,36 @@ +package apptive.devlog.infrastructure.redis.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(); + } + + @Bean + public StringRedisSerializer stringRedisSerializer() { + return new StringRedisSerializer(); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory, StringRedisSerializer stringRedisSerializer) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + + redisTemplate.setKeySerializer(stringRedisSerializer); + redisTemplate.setValueSerializer(stringRedisSerializer); + redisTemplate.setHashKeySerializer(stringRedisSerializer); + redisTemplate.setHashValueSerializer(stringRedisSerializer); + + redisTemplate.afterPropertiesSet(); + + return redisTemplate; + } +} diff --git a/src/main/java/apptive/devlog/infrastructure/redis/repository/RedisRepository.java b/src/main/java/apptive/devlog/infrastructure/redis/repository/RedisRepository.java new file mode 100644 index 0000000..55d2c2a --- /dev/null +++ b/src/main/java/apptive/devlog/infrastructure/redis/repository/RedisRepository.java @@ -0,0 +1,49 @@ +package apptive.devlog.infrastructure.redis.repository; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +@Repository +public class RedisRepository { + private final long accessTokenExpiration; + private final long refreshTokenExpiration; + private final RedisTemplate redisTemplate; + private static final Duration TTL = Duration.ofMinutes(10); + + public RedisRepository( + @Value("${jwt.access-token-expiration}") long accessTokenExpiration, + @Value("${jwt.refresh-token-expiration}") long refreshTokenExpiration, + RedisTemplate redisTemplate) { + this.accessTokenExpiration = accessTokenExpiration; + this.refreshTokenExpiration = refreshTokenExpiration; + this.redisTemplate = redisTemplate; + } + + public void saveAccessToken(String accessToken, String email) { + redisTemplate.opsForValue().set("AT:" + accessToken, email, accessTokenExpiration, TimeUnit.MILLISECONDS); + } + + public boolean hasAccessToken(String accessToken) { + return Boolean.TRUE.equals(redisTemplate.hasKey("AT:" + accessToken)); + } + + public void deleteAccessToken(String accessToken) { + redisTemplate.delete("AT:" + accessToken); + } + + public void saveRefreshToken(String refreshToken, String email) { + redisTemplate.opsForValue().set("RT:" + refreshToken, email, refreshTokenExpiration, TimeUnit.MILLISECONDS); + } + + public boolean hasRefreshToken(String refreshToken) { + return Boolean.TRUE.equals(redisTemplate.hasKey("RT:" + refreshToken)); + } + + public void deleteRefreshToken(String refreshToken) { + redisTemplate.delete("RT:" + refreshToken); + } +} diff --git a/src/main/java/apptive/devlog/support/testconfig/redis/RedisInitializer.java b/src/main/java/apptive/devlog/support/testconfig/redis/RedisInitializer.java new file mode 100644 index 0000000..7a6ab05 --- /dev/null +++ b/src/main/java/apptive/devlog/support/testconfig/redis/RedisInitializer.java @@ -0,0 +1,18 @@ +package apptive.devlog.support.testconfig.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class RedisInitializer implements ApplicationRunner { + private final RedisTemplate redisTemplate; + + @Override + public void run(ApplicationArguments args) { + redisTemplate.delete(redisTemplate.keys("*")); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 95619f3..8f41cb1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,90 @@ +# =============================== +# SPRING CONFIG +# =============================== spring.application.name=devlog +spring.datasource.url=jdbc:mysql://localhost:3306/devlog?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 +spring.datasource.username=root +spring.datasource.password= +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# =============================== +# JPA CONFIG +# =============================== +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=create +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.show_sql=true +spring.jpa.open-in-view=false +spring.jpa.show-sql=true + +# =============================== +# SERVER CONFIG +# =============================== +server.port=8080 +server.ssl.enabled=false + +# =============================== +# REDIS CONFIG +# =============================== +spring.data.redis.host=localhost +spring.data.redis.port=6379 +spring.data.redis.password= + +# =============================== +# LOGGING +# =============================== +logging.level.org.springframework.security=DEBUG +logging.level.com.apptive.devlog=DEBUG + +# =============================== +# JWT CONFIG +# =============================== +jwt.secret= +jwt.access-token-expiration=3600000 +jwt.refresh-token-expiration=1209600000 + +# =============================== +# OAUTH2 CONFIG - GOOGLE +# =============================== +spring.security.oauth2.client.registration.google.client-id= +spring.security.oauth2.client.registration.google.client-secret= +spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.google.scope=profile,email +spring.security.oauth2.client.registration.google.client-name=Google +spring.security.oauth2.client.registration.google.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth +spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token +spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo +spring.security.oauth2.client.provider.google.user-name-attribute=sub + +# =============================== +# OAUTH2 CONFIG - NAVER +# =============================== +spring.security.oauth2.client.registration.naver.client-id= +spring.security.oauth2.client.registration.naver.client-secret= +spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.naver.scope=name,email +spring.security.oauth2.client.registration.naver.client-name=Naver +spring.security.oauth2.client.registration.naver.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize +spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token +spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me +spring.security.oauth2.client.provider.naver.user-name-attribute=response + +# =============================== +# OAUTH2 CONFIG - KAKAO +# =============================== +spring.security.oauth2.client.registration.kakao.client-id= +spring.security.oauth2.client.registration.kakao.client-secret= +spring.security.oauth2.client.registration.kakao.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email +spring.security.oauth2.client.registration.kakao.client-name=Kakao +spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize +spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token +spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me +spring.security.oauth2.client.provider.kakao.user-name-attribute=id diff --git a/src/test/java/apptive/devlog/domain/auth/controller/AuthControllerTest.java b/src/test/java/apptive/devlog/domain/auth/controller/AuthControllerTest.java new file mode 100644 index 0000000..6428236 --- /dev/null +++ b/src/test/java/apptive/devlog/domain/auth/controller/AuthControllerTest.java @@ -0,0 +1,4 @@ +package apptive.devlog.domain.auth.controller; + +public class AuthControllerTest { +} diff --git a/src/test/java/apptive/devlog/domain/auth/service/AuthServiceTest.java b/src/test/java/apptive/devlog/domain/auth/service/AuthServiceTest.java new file mode 100644 index 0000000..4a00ce1 --- /dev/null +++ b/src/test/java/apptive/devlog/domain/auth/service/AuthServiceTest.java @@ -0,0 +1,4 @@ +package apptive.devlog.domain.auth.service; + +public class AuthServiceTest { +} diff --git a/src/test/java/apptive/devlog/domain/oauth2/handler/OAuth2FailureHandlerTest.java b/src/test/java/apptive/devlog/domain/oauth2/handler/OAuth2FailureHandlerTest.java new file mode 100644 index 0000000..2227f70 --- /dev/null +++ b/src/test/java/apptive/devlog/domain/oauth2/handler/OAuth2FailureHandlerTest.java @@ -0,0 +1,4 @@ +package apptive.devlog.domain.oauth2.handler; + +public class OAuth2FailureHandlerTest { +} diff --git a/src/test/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandlerTest.java b/src/test/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandlerTest.java new file mode 100644 index 0000000..1bc6c7a --- /dev/null +++ b/src/test/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandlerTest.java @@ -0,0 +1,4 @@ +package apptive.devlog.domain.oauth2.handler; + +public class OAuth2SuccessHandlerTest { +} diff --git a/src/test/java/apptive/devlog/domain/oauth2/service/CustomOAuth2UserServiceTest.java b/src/test/java/apptive/devlog/domain/oauth2/service/CustomOAuth2UserServiceTest.java new file mode 100644 index 0000000..c0f36a9 --- /dev/null +++ b/src/test/java/apptive/devlog/domain/oauth2/service/CustomOAuth2UserServiceTest.java @@ -0,0 +1,4 @@ +package apptive.devlog.domain.oauth2.service; + +public class CustomOAuth2UserServiceTest { +} diff --git a/src/test/java/apptive/devlog/domain/user/controller/UserControllerTest.java b/src/test/java/apptive/devlog/domain/user/controller/UserControllerTest.java new file mode 100644 index 0000000..6fffcb7 --- /dev/null +++ b/src/test/java/apptive/devlog/domain/user/controller/UserControllerTest.java @@ -0,0 +1,4 @@ +package apptive.devlog.domain.user.controller; + +public class UserControllerTest { +} diff --git a/src/test/java/apptive/devlog/domain/user/service/UserServiceTest.java b/src/test/java/apptive/devlog/domain/user/service/UserServiceTest.java new file mode 100644 index 0000000..9e3f06a --- /dev/null +++ b/src/test/java/apptive/devlog/domain/user/service/UserServiceTest.java @@ -0,0 +1,4 @@ +package apptive.devlog.domain.user.service; + +public class UserServiceTest { +} From 136cdfd089efb32044785080facbe5aac330d7f5 Mon Sep 17 00:00:00 2001 From: qqwwee771441 <104188251+qqwwee771441@users.noreply.github.com> Date: Wed, 9 Apr 2025 04:14:00 +0900 Subject: [PATCH 03/11] feat: hw1 --- .gitignore | 2 +- .../common/response/api/ApiResponse.java | 96 +++++++++++++++ .../error/exception/CustomException.java | 4 +- .../exception/DtoBindingFailedException.java | 12 ++ .../exception/DuplicateEmailException.java | 4 +- .../InvalidEmailOrPasswordException.java | 4 +- .../InvalidOAuth2ResponseException.java | 7 ++ .../exception/InvalidPasswordException.java | 4 +- .../exception/InvalidProviderException.java | 4 +- .../InvalidRefreshTokenException.java | 4 +- .../InvalidStateFormatException.java | 4 +- .../exception/InvalidTokenException.java | 4 +- .../OAuth2TokenRequestException.java | 4 +- .../exception/OAuth2UserInfoException.java | 4 +- .../TokenInjectionFailedException.java | 16 +++ .../UnauthenticatedUserException.java | 16 +++ .../UnsupportedProviderException.java | 7 ++ .../exception/UserNotFoundException.java | 4 +- .../error/handler/GlobalExceptionHandler.java | 81 +++++++++++++ .../response/error/model/ErrorCode.java | 2 +- .../response/error/model/ErrorResponse.java | 2 +- .../auth/controller/AuthController.java | 11 +- .../domain/auth/service/AuthService.java | 24 ++-- .../controller/OAuth2PkceController.java | 4 +- .../oauth2/dto/OAuth2CallbackRequestDto.java | 10 +- .../oauth2/handler/OAuth2FailureHandler.java | 31 +++-- .../oauth2/handler/OAuth2SuccessHandler.java | 26 ++-- .../domain/oauth2/model/OAuth2Attributes.java | 43 ++++--- .../service/CustomOAuth2UserService.java | 30 +++-- .../oauth2/service/OAuth2PkceService.java | 114 ++++++++++++++---- .../user/controller/UserController.java | 13 +- .../domain/user/service/UserService.java | 5 +- .../resolver/InjectEmailArgumentResolver.java | 41 ++++--- .../resolver/InjectTokenArgumentResolver.java | 50 ++++++-- .../global/response/api/ApiResponse.java | 21 ---- .../TokenInjectionFailedException.java | 9 -- .../UnauthenticatedUserException.java | 9 -- .../error/handler/GlobalExceptionHandler.java | 72 ----------- .../security/config/SecurityConfig.java | 33 ++--- .../filter/JwtAuthenticationFilter.java | 19 ++- .../global/security/jwt/JwtTokenProvider.java | 33 +++-- .../security/model/CustomUserDetails.java | 53 ++++++++ .../service/CustomUserDetailsService.java | 3 +- .../redis/config/RedisConfig.java | 25 ++-- .../redis/repository/RedisRepository.java | 23 ++-- .../testconfig/redis/RedisInitializer.java | 15 ++- .../resources/application-local.properties | 80 ++++++++++++ src/main/resources/application.properties | 88 +------------- 48 files changed, 744 insertions(+), 426 deletions(-) create mode 100644 src/main/java/apptive/devlog/common/response/api/ApiResponse.java rename src/main/java/apptive/devlog/{global => common}/response/error/exception/CustomException.java (81%) create mode 100644 src/main/java/apptive/devlog/common/response/error/exception/DtoBindingFailedException.java rename src/main/java/apptive/devlog/{global => common}/response/error/exception/DuplicateEmailException.java (56%) rename src/main/java/apptive/devlog/{global => common}/response/error/exception/InvalidEmailOrPasswordException.java (60%) create mode 100644 src/main/java/apptive/devlog/common/response/error/exception/InvalidOAuth2ResponseException.java rename src/main/java/apptive/devlog/{global => common}/response/error/exception/InvalidPasswordException.java (57%) rename src/main/java/apptive/devlog/{global => common}/response/error/exception/InvalidProviderException.java (57%) rename src/main/java/apptive/devlog/{global => common}/response/error/exception/InvalidRefreshTokenException.java (59%) rename src/main/java/apptive/devlog/{global => common}/response/error/exception/InvalidStateFormatException.java (58%) rename src/main/java/apptive/devlog/{global => common}/response/error/exception/InvalidTokenException.java (55%) rename src/main/java/apptive/devlog/{global => common}/response/error/exception/OAuth2TokenRequestException.java (62%) rename src/main/java/apptive/devlog/{global => common}/response/error/exception/OAuth2UserInfoException.java (61%) create mode 100644 src/main/java/apptive/devlog/common/response/error/exception/TokenInjectionFailedException.java create mode 100644 src/main/java/apptive/devlog/common/response/error/exception/UnauthenticatedUserException.java create mode 100644 src/main/java/apptive/devlog/common/response/error/exception/UnsupportedProviderException.java rename src/main/java/apptive/devlog/{global => common}/response/error/exception/UserNotFoundException.java (56%) create mode 100644 src/main/java/apptive/devlog/common/response/error/handler/GlobalExceptionHandler.java rename src/main/java/apptive/devlog/{global => common}/response/error/model/ErrorCode.java (96%) rename src/main/java/apptive/devlog/{global => common}/response/error/model/ErrorResponse.java (78%) delete mode 100644 src/main/java/apptive/devlog/global/response/api/ApiResponse.java delete mode 100644 src/main/java/apptive/devlog/global/response/error/exception/TokenInjectionFailedException.java delete mode 100644 src/main/java/apptive/devlog/global/response/error/exception/UnauthenticatedUserException.java delete mode 100644 src/main/java/apptive/devlog/global/response/error/handler/GlobalExceptionHandler.java create mode 100644 src/main/java/apptive/devlog/global/security/model/CustomUserDetails.java create mode 100644 src/main/resources/application-local.properties diff --git a/.gitignore b/.gitignore index d48f7ac..fce8590 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,4 @@ out/ ### VS Code ### .vscode/ -src/main/resources/application.properties +src/main/resources/application-secret.properties diff --git a/src/main/java/apptive/devlog/common/response/api/ApiResponse.java b/src/main/java/apptive/devlog/common/response/api/ApiResponse.java new file mode 100644 index 0000000..55bc1aa --- /dev/null +++ b/src/main/java/apptive/devlog/common/response/api/ApiResponse.java @@ -0,0 +1,96 @@ +package apptive.devlog.common.response.api; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ApiResponse { + + private final boolean success; + private final int status; + private final String message; + private final T data; + private final LocalDateTime timestamp; + + @Builder + private ApiResponse(boolean success, HttpStatus httpStatus, String message, T data, LocalDateTime timestamp) { + this.success = success; + this.status = httpStatus.value(); + this.message = message; + this.data = data; + this.timestamp = timestamp != null ? timestamp : LocalDateTime.now(); + } + + // ===== Success ===== + + public static ApiResponse success(HttpStatus status, String message, T data) { + return ApiResponse.builder() + .success(true) + .httpStatus(status) + .message(message) + .data(data) + .build(); + } + + public static ApiResponse success(HttpStatus status, T data) { + return ApiResponse.builder() + .success(true) + .httpStatus(status) + .message("요청이 성공했습니다.") + .data(data) + .build(); + } + + public static ApiResponse success(HttpStatus status, String message) { + return ApiResponse.builder() + .success(true) + .httpStatus(status) + .message(message) + .build(); + } + + public static ApiResponse success(HttpStatus status) { + return ApiResponse.builder() + .success(true) + .httpStatus(status) + .message("요청이 성공했습니다.") + .build(); + } + + // 기본 200 OK + public static ApiResponse ok(T data) { + return success(HttpStatus.OK, data); + } + + public static ApiResponse created(T data) { + return success(HttpStatus.CREATED, data); + } + + public static ApiResponse noContent() { + return success(HttpStatus.NO_CONTENT, (Void) null); + } + + // ===== Error ===== + + public static ApiResponse error(HttpStatus status, String message) { + return ApiResponse.builder() + .success(false) + .httpStatus(status) + .message(message) + .build(); + } + + public static ApiResponse error(HttpStatus status, String message, T data) { + return ApiResponse.builder() + .success(false) + .httpStatus(status) + .message(message) + .data(data) + .build(); + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/exception/CustomException.java b/src/main/java/apptive/devlog/common/response/error/exception/CustomException.java similarity index 81% rename from src/main/java/apptive/devlog/global/response/error/exception/CustomException.java rename to src/main/java/apptive/devlog/common/response/error/exception/CustomException.java index eec84ad..709f003 100644 --- a/src/main/java/apptive/devlog/global/response/error/exception/CustomException.java +++ b/src/main/java/apptive/devlog/common/response/error/exception/CustomException.java @@ -1,6 +1,6 @@ -package apptive.devlog.global.response.error.exception; +package apptive.devlog.common.response.error.exception; -import apptive.devlog.global.response.error.model.ErrorCode; +import apptive.devlog.common.response.error.model.ErrorCode; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/apptive/devlog/common/response/error/exception/DtoBindingFailedException.java b/src/main/java/apptive/devlog/common/response/error/exception/DtoBindingFailedException.java new file mode 100644 index 0000000..f385cff --- /dev/null +++ b/src/main/java/apptive/devlog/common/response/error/exception/DtoBindingFailedException.java @@ -0,0 +1,12 @@ +package apptive.devlog.common.response.error.exception; + +public class DtoBindingFailedException extends RuntimeException { + + public DtoBindingFailedException(String message) { + super(message); + } + + public DtoBindingFailedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/exception/DuplicateEmailException.java b/src/main/java/apptive/devlog/common/response/error/exception/DuplicateEmailException.java similarity index 56% rename from src/main/java/apptive/devlog/global/response/error/exception/DuplicateEmailException.java rename to src/main/java/apptive/devlog/common/response/error/exception/DuplicateEmailException.java index a75d1b8..c7c51fc 100644 --- a/src/main/java/apptive/devlog/global/response/error/exception/DuplicateEmailException.java +++ b/src/main/java/apptive/devlog/common/response/error/exception/DuplicateEmailException.java @@ -1,6 +1,6 @@ -package apptive.devlog.global.response.error.exception; +package apptive.devlog.common.response.error.exception; -import apptive.devlog.global.response.error.model.ErrorCode; +import apptive.devlog.common.response.error.model.ErrorCode; public class DuplicateEmailException extends CustomException { public DuplicateEmailException() { diff --git a/src/main/java/apptive/devlog/global/response/error/exception/InvalidEmailOrPasswordException.java b/src/main/java/apptive/devlog/common/response/error/exception/InvalidEmailOrPasswordException.java similarity index 60% rename from src/main/java/apptive/devlog/global/response/error/exception/InvalidEmailOrPasswordException.java rename to src/main/java/apptive/devlog/common/response/error/exception/InvalidEmailOrPasswordException.java index 959ee2e..071919b 100644 --- a/src/main/java/apptive/devlog/global/response/error/exception/InvalidEmailOrPasswordException.java +++ b/src/main/java/apptive/devlog/common/response/error/exception/InvalidEmailOrPasswordException.java @@ -1,6 +1,6 @@ -package apptive.devlog.global.response.error.exception; +package apptive.devlog.common.response.error.exception; -import apptive.devlog.global.response.error.model.ErrorCode; +import apptive.devlog.common.response.error.model.ErrorCode; public class InvalidEmailOrPasswordException extends CustomException { public InvalidEmailOrPasswordException() { diff --git a/src/main/java/apptive/devlog/common/response/error/exception/InvalidOAuth2ResponseException.java b/src/main/java/apptive/devlog/common/response/error/exception/InvalidOAuth2ResponseException.java new file mode 100644 index 0000000..0744cbf --- /dev/null +++ b/src/main/java/apptive/devlog/common/response/error/exception/InvalidOAuth2ResponseException.java @@ -0,0 +1,7 @@ +package apptive.devlog.common.response.error.exception; + +public class InvalidOAuth2ResponseException extends RuntimeException { + public InvalidOAuth2ResponseException(String key) { + super("OAuth2 응답에서 예상한 '" + key + "' 구조가 올바르지 않습니다."); + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/exception/InvalidPasswordException.java b/src/main/java/apptive/devlog/common/response/error/exception/InvalidPasswordException.java similarity index 57% rename from src/main/java/apptive/devlog/global/response/error/exception/InvalidPasswordException.java rename to src/main/java/apptive/devlog/common/response/error/exception/InvalidPasswordException.java index 8391727..f71d69f 100644 --- a/src/main/java/apptive/devlog/global/response/error/exception/InvalidPasswordException.java +++ b/src/main/java/apptive/devlog/common/response/error/exception/InvalidPasswordException.java @@ -1,6 +1,6 @@ -package apptive.devlog.global.response.error.exception; +package apptive.devlog.common.response.error.exception; -import apptive.devlog.global.response.error.model.ErrorCode; +import apptive.devlog.common.response.error.model.ErrorCode; public class InvalidPasswordException extends CustomException { public InvalidPasswordException() { diff --git a/src/main/java/apptive/devlog/global/response/error/exception/InvalidProviderException.java b/src/main/java/apptive/devlog/common/response/error/exception/InvalidProviderException.java similarity index 57% rename from src/main/java/apptive/devlog/global/response/error/exception/InvalidProviderException.java rename to src/main/java/apptive/devlog/common/response/error/exception/InvalidProviderException.java index 9d9c490..a283203 100644 --- a/src/main/java/apptive/devlog/global/response/error/exception/InvalidProviderException.java +++ b/src/main/java/apptive/devlog/common/response/error/exception/InvalidProviderException.java @@ -1,6 +1,6 @@ -package apptive.devlog.global.response.error.exception; +package apptive.devlog.common.response.error.exception; -import apptive.devlog.global.response.error.model.ErrorCode; +import apptive.devlog.common.response.error.model.ErrorCode; public class InvalidProviderException extends CustomException { public InvalidProviderException() { diff --git a/src/main/java/apptive/devlog/global/response/error/exception/InvalidRefreshTokenException.java b/src/main/java/apptive/devlog/common/response/error/exception/InvalidRefreshTokenException.java similarity index 59% rename from src/main/java/apptive/devlog/global/response/error/exception/InvalidRefreshTokenException.java rename to src/main/java/apptive/devlog/common/response/error/exception/InvalidRefreshTokenException.java index a59d910..d187236 100644 --- a/src/main/java/apptive/devlog/global/response/error/exception/InvalidRefreshTokenException.java +++ b/src/main/java/apptive/devlog/common/response/error/exception/InvalidRefreshTokenException.java @@ -1,6 +1,6 @@ -package apptive.devlog.global.response.error.exception; +package apptive.devlog.common.response.error.exception; -import apptive.devlog.global.response.error.model.ErrorCode; +import apptive.devlog.common.response.error.model.ErrorCode; public class InvalidRefreshTokenException extends CustomException { public InvalidRefreshTokenException() { diff --git a/src/main/java/apptive/devlog/global/response/error/exception/InvalidStateFormatException.java b/src/main/java/apptive/devlog/common/response/error/exception/InvalidStateFormatException.java similarity index 58% rename from src/main/java/apptive/devlog/global/response/error/exception/InvalidStateFormatException.java rename to src/main/java/apptive/devlog/common/response/error/exception/InvalidStateFormatException.java index a15c2dc..c1db771 100644 --- a/src/main/java/apptive/devlog/global/response/error/exception/InvalidStateFormatException.java +++ b/src/main/java/apptive/devlog/common/response/error/exception/InvalidStateFormatException.java @@ -1,6 +1,6 @@ -package apptive.devlog.global.response.error.exception; +package apptive.devlog.common.response.error.exception; -import apptive.devlog.global.response.error.model.ErrorCode; +import apptive.devlog.common.response.error.model.ErrorCode; public class InvalidStateFormatException extends CustomException { public InvalidStateFormatException() { diff --git a/src/main/java/apptive/devlog/global/response/error/exception/InvalidTokenException.java b/src/main/java/apptive/devlog/common/response/error/exception/InvalidTokenException.java similarity index 55% rename from src/main/java/apptive/devlog/global/response/error/exception/InvalidTokenException.java rename to src/main/java/apptive/devlog/common/response/error/exception/InvalidTokenException.java index 392c764..95de2c4 100644 --- a/src/main/java/apptive/devlog/global/response/error/exception/InvalidTokenException.java +++ b/src/main/java/apptive/devlog/common/response/error/exception/InvalidTokenException.java @@ -1,6 +1,6 @@ -package apptive.devlog.global.response.error.exception; +package apptive.devlog.common.response.error.exception; -import apptive.devlog.global.response.error.model.ErrorCode; +import apptive.devlog.common.response.error.model.ErrorCode; public class InvalidTokenException extends CustomException { public InvalidTokenException() { diff --git a/src/main/java/apptive/devlog/global/response/error/exception/OAuth2TokenRequestException.java b/src/main/java/apptive/devlog/common/response/error/exception/OAuth2TokenRequestException.java similarity index 62% rename from src/main/java/apptive/devlog/global/response/error/exception/OAuth2TokenRequestException.java rename to src/main/java/apptive/devlog/common/response/error/exception/OAuth2TokenRequestException.java index 3feda0a..5934e9e 100644 --- a/src/main/java/apptive/devlog/global/response/error/exception/OAuth2TokenRequestException.java +++ b/src/main/java/apptive/devlog/common/response/error/exception/OAuth2TokenRequestException.java @@ -1,6 +1,6 @@ -package apptive.devlog.global.response.error.exception; +package apptive.devlog.common.response.error.exception; -import apptive.devlog.global.response.error.model.ErrorCode; +import apptive.devlog.common.response.error.model.ErrorCode; public class OAuth2TokenRequestException extends CustomException { public OAuth2TokenRequestException(String message) { diff --git a/src/main/java/apptive/devlog/global/response/error/exception/OAuth2UserInfoException.java b/src/main/java/apptive/devlog/common/response/error/exception/OAuth2UserInfoException.java similarity index 61% rename from src/main/java/apptive/devlog/global/response/error/exception/OAuth2UserInfoException.java rename to src/main/java/apptive/devlog/common/response/error/exception/OAuth2UserInfoException.java index 507113b..ac33186 100644 --- a/src/main/java/apptive/devlog/global/response/error/exception/OAuth2UserInfoException.java +++ b/src/main/java/apptive/devlog/common/response/error/exception/OAuth2UserInfoException.java @@ -1,6 +1,6 @@ -package apptive.devlog.global.response.error.exception; +package apptive.devlog.common.response.error.exception; -import apptive.devlog.global.response.error.model.ErrorCode; +import apptive.devlog.common.response.error.model.ErrorCode; public class OAuth2UserInfoException extends CustomException { public OAuth2UserInfoException(String message) { diff --git a/src/main/java/apptive/devlog/common/response/error/exception/TokenInjectionFailedException.java b/src/main/java/apptive/devlog/common/response/error/exception/TokenInjectionFailedException.java new file mode 100644 index 0000000..b55c780 --- /dev/null +++ b/src/main/java/apptive/devlog/common/response/error/exception/TokenInjectionFailedException.java @@ -0,0 +1,16 @@ +package apptive.devlog.common.response.error.exception; + +public class TokenInjectionFailedException extends RuntimeException { + + public TokenInjectionFailedException(String message) { + super("[Token 주입 실패] " + message); + } + + public TokenInjectionFailedException(String fieldName, Throwable cause) { + super(String.format("[Token 주입 실패] 필드 '%s'에 접근 중 오류 발생", fieldName), cause); + } + + public TokenInjectionFailedException(Throwable cause) { + super("[Token 주입 실패] 원인 미상", cause); + } +} diff --git a/src/main/java/apptive/devlog/common/response/error/exception/UnauthenticatedUserException.java b/src/main/java/apptive/devlog/common/response/error/exception/UnauthenticatedUserException.java new file mode 100644 index 0000000..44a2030 --- /dev/null +++ b/src/main/java/apptive/devlog/common/response/error/exception/UnauthenticatedUserException.java @@ -0,0 +1,16 @@ +package apptive.devlog.common.response.error.exception; + +public class UnauthenticatedUserException extends RuntimeException { + + public UnauthenticatedUserException() { + super("인증되지 않은 사용자입니다."); + } + + public UnauthenticatedUserException(String message) { + super(message); + } + + public UnauthenticatedUserException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/apptive/devlog/common/response/error/exception/UnsupportedProviderException.java b/src/main/java/apptive/devlog/common/response/error/exception/UnsupportedProviderException.java new file mode 100644 index 0000000..c566746 --- /dev/null +++ b/src/main/java/apptive/devlog/common/response/error/exception/UnsupportedProviderException.java @@ -0,0 +1,7 @@ +package apptive.devlog.common.response.error.exception; + +public class UnsupportedProviderException extends RuntimeException { + public UnsupportedProviderException(String provider) { + super("지원하지 않는 OAuth2 Provider입니다: " + provider); + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/exception/UserNotFoundException.java b/src/main/java/apptive/devlog/common/response/error/exception/UserNotFoundException.java similarity index 56% rename from src/main/java/apptive/devlog/global/response/error/exception/UserNotFoundException.java rename to src/main/java/apptive/devlog/common/response/error/exception/UserNotFoundException.java index 6586f3c..343c81b 100644 --- a/src/main/java/apptive/devlog/global/response/error/exception/UserNotFoundException.java +++ b/src/main/java/apptive/devlog/common/response/error/exception/UserNotFoundException.java @@ -1,6 +1,6 @@ -package apptive.devlog.global.response.error.exception; +package apptive.devlog.common.response.error.exception; -import apptive.devlog.global.response.error.model.ErrorCode; +import apptive.devlog.common.response.error.model.ErrorCode; public class UserNotFoundException extends CustomException { public UserNotFoundException() { diff --git a/src/main/java/apptive/devlog/common/response/error/handler/GlobalExceptionHandler.java b/src/main/java/apptive/devlog/common/response/error/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..1d0ce24 --- /dev/null +++ b/src/main/java/apptive/devlog/common/response/error/handler/GlobalExceptionHandler.java @@ -0,0 +1,81 @@ +package apptive.devlog.common.response.error.handler; + +import apptive.devlog.common.response.error.exception.*; +import apptive.devlog.common.response.error.model.ErrorCode; +import apptive.devlog.common.response.error.model.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + // 공통 응답 빌더 (기본 메시지 사용) + private ResponseEntity buildResponse(ErrorCode errorCode) { + return ResponseEntity + .status(errorCode.getStatus()) + .body(new ErrorResponse( + errorCode.getStatus(), + errorCode.getMessage(), + errorCode.getCode())); + } + + // 공통 응답 빌더 (커스텀 메시지 사용) + private ResponseEntity buildResponse(ErrorCode errorCode, String customMessage) { + return ResponseEntity + .status(errorCode.getStatus()) + .body(new ErrorResponse( + errorCode.getStatus(), + customMessage, + errorCode.getCode())); + } + + // 공통 커스텀 예외 처리 + @ExceptionHandler(CustomException.class) + public ResponseEntity handleCustomException(CustomException ex) { + log.warn("CustomException [{}]: {}", ex.getErrorCode().name(), ex.getMessage(), ex); + return buildResponse(ex.getErrorCode()); + } + + // DTO 바인딩 예외 처리 (직접 만든 예외 + Spring 기본 예외) + @ExceptionHandler({DtoBindingFailedException.class, MethodArgumentNotValidException.class}) + public ResponseEntity handleBindingException(Exception ex) { + log.warn("BindingException: {}", ex.getMessage(), ex); + return buildResponse(ErrorCode.INVALID_STATE_FORMAT, ex.getMessage()); + } + + // IllegalArgumentException 처리 + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgument(IllegalArgumentException ex) { + log.warn("IllegalArgumentException: {}", ex.getMessage(), ex); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + ex.getMessage(), + "BAD_REQUEST")); + } + + // 런타임 예외 처리 (디버깅 및 추적용) + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException ex) { + log.error("RuntimeException: {}", ex.getMessage(), ex); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + ex.getMessage(), + "INTERNAL_ERROR")); + } + + // 마지막 처리: 예상하지 못한 예외 + @ExceptionHandler(Exception.class) + public ResponseEntity handleUnhandledException(Exception ex) { + log.error("Unhandled Exception: {}", ex.getMessage(), ex); + return buildResponse(ErrorCode.UNKNOWN_ERROR); + } +} diff --git a/src/main/java/apptive/devlog/global/response/error/model/ErrorCode.java b/src/main/java/apptive/devlog/common/response/error/model/ErrorCode.java similarity index 96% rename from src/main/java/apptive/devlog/global/response/error/model/ErrorCode.java rename to src/main/java/apptive/devlog/common/response/error/model/ErrorCode.java index e8d1d99..ca0d112 100644 --- a/src/main/java/apptive/devlog/global/response/error/model/ErrorCode.java +++ b/src/main/java/apptive/devlog/common/response/error/model/ErrorCode.java @@ -1,4 +1,4 @@ -package apptive.devlog.global.response.error.model; +package apptive.devlog.common.response.error.model; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/apptive/devlog/global/response/error/model/ErrorResponse.java b/src/main/java/apptive/devlog/common/response/error/model/ErrorResponse.java similarity index 78% rename from src/main/java/apptive/devlog/global/response/error/model/ErrorResponse.java rename to src/main/java/apptive/devlog/common/response/error/model/ErrorResponse.java index ffb7dd6..58b0c06 100644 --- a/src/main/java/apptive/devlog/global/response/error/model/ErrorResponse.java +++ b/src/main/java/apptive/devlog/common/response/error/model/ErrorResponse.java @@ -1,4 +1,4 @@ -package apptive.devlog.global.response.error.model; +package apptive.devlog.common.response.error.model; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java index f3f038f..7ad7100 100644 --- a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java +++ b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java @@ -3,9 +3,10 @@ import apptive.devlog.domain.auth.dto.*; import apptive.devlog.global.annotation.InjectToken; import apptive.devlog.domain.auth.service.AuthService; -import apptive.devlog.global.response.api.ApiResponse; +import apptive.devlog.common.response.api.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -18,24 +19,24 @@ public class AuthController { @PostMapping("/signup") public ResponseEntity> signup(@Valid @RequestBody UserSignupRequestDto requestDto) { UserSignupResponseDto responseDto = authService.signup(requestDto); - return ResponseEntity.ok(ApiResponse.success(responseDto)); + return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.created(responseDto)); } @PostMapping("/login") public ResponseEntity> login(@Valid @RequestBody UserLoginRequestDto requestDto) { UserLoginResponseDto responseDto = authService.login(requestDto); - return ResponseEntity.ok(ApiResponse.success(responseDto)); + return ResponseEntity.ok(ApiResponse.ok(responseDto)); } @PostMapping("/refresh") public ResponseEntity> refresh(@Valid @InjectToken UserRefreshRequestDto requestDto) { UserRefreshResponseDto responseDto = authService.refresh(requestDto); - return ResponseEntity.ok(ApiResponse.success(responseDto)); + return ResponseEntity.ok(ApiResponse.ok(responseDto)); } @PostMapping("/logout") public ResponseEntity> logout(@Valid @InjectToken UserLogoutRequestDto requestDto) { authService.logout(requestDto); - return ResponseEntity.ok(ApiResponse.success("logout success", null)); + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(ApiResponse.noContent()); } } diff --git a/src/main/java/apptive/devlog/domain/auth/service/AuthService.java b/src/main/java/apptive/devlog/domain/auth/service/AuthService.java index 9e9b324..2d31c00 100644 --- a/src/main/java/apptive/devlog/domain/auth/service/AuthService.java +++ b/src/main/java/apptive/devlog/domain/auth/service/AuthService.java @@ -1,11 +1,8 @@ package apptive.devlog.domain.auth.service; +import apptive.devlog.common.response.error.exception.*; import apptive.devlog.domain.auth.dto.*; import apptive.devlog.domain.user.enums.Provider; -import apptive.devlog.global.response.error.exception.DuplicateEmailException; -import apptive.devlog.global.response.error.exception.InvalidEmailOrPasswordException; -import apptive.devlog.global.response.error.exception.InvalidProviderException; -import apptive.devlog.global.response.error.exception.InvalidRefreshTokenException; import apptive.devlog.global.security.jwt.JwtTokenProvider; import apptive.devlog.infrastructure.redis.repository.RedisRepository; import apptive.devlog.domain.user.entity.User; @@ -35,7 +32,7 @@ public UserSignupResponseDto signup(UserSignupRequestDto requestDto) { @Transactional public UserLoginResponseDto login(UserLoginRequestDto requestDto) { - User user = userRepository.findByEmail(requestDto.getEmail()).orElseThrow(() -> new IllegalArgumentException("Invalid email or password")); + User user = userRepository.findByEmail(requestDto.getEmail()).orElseThrow(UserNotFoundException::new); if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) { throw new InvalidEmailOrPasswordException(); } @@ -49,30 +46,27 @@ public UserLoginResponseDto login(UserLoginRequestDto requestDto) { @Transactional public UserRefreshResponseDto refresh(UserRefreshRequestDto requestDto) { - String accessToken = requestDto.getAccessToken(); - String refreshToken = requestDto.getRefreshToken(); + final String accessToken = requestDto.getAccessToken(); + final String refreshToken = requestDto.getRefreshToken(); if (!jwtTokenProvider.validateRefreshToken(refreshToken)) { throw new InvalidRefreshTokenException(); } - String email = jwtTokenProvider.getEmailFromToken(refreshToken); + final String email = jwtTokenProvider.getEmailFromToken(refreshToken); redisRepository.deleteAccessToken(accessToken); redisRepository.deleteRefreshToken(refreshToken); - String newAccessToken = jwtTokenProvider.generateAccessToken(email); - String newRefreshToken = jwtTokenProvider.generateRefreshToken(email); + final String newAccessToken = jwtTokenProvider.generateAccessToken(email); + final String newRefreshToken = jwtTokenProvider.generateRefreshToken(email); return new UserRefreshResponseDto(newAccessToken, newRefreshToken); } @Transactional public void logout(UserLogoutRequestDto requestDto) { - String accessToken = requestDto.getAccessToken(); - String refreshToken = requestDto.getRefreshToken(); - - redisRepository.deleteAccessToken(accessToken); - redisRepository.deleteRefreshToken(refreshToken); + redisRepository.deleteAccessToken(requestDto.getAccessToken()); + redisRepository.deleteRefreshToken(requestDto.getRefreshToken()); } } diff --git a/src/main/java/apptive/devlog/domain/oauth2/controller/OAuth2PkceController.java b/src/main/java/apptive/devlog/domain/oauth2/controller/OAuth2PkceController.java index bdeab88..d0708af 100644 --- a/src/main/java/apptive/devlog/domain/oauth2/controller/OAuth2PkceController.java +++ b/src/main/java/apptive/devlog/domain/oauth2/controller/OAuth2PkceController.java @@ -3,7 +3,7 @@ import apptive.devlog.domain.oauth2.dto.OAuth2CallbackRequestDto; import apptive.devlog.domain.oauth2.dto.OAuth2CallbackResponseDto; import apptive.devlog.domain.oauth2.service.OAuth2PkceService; -import apptive.devlog.global.response.api.ApiResponse; +import apptive.devlog.common.response.api.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -19,6 +19,6 @@ public class OAuth2PkceController { @PostMapping("/pkce/callback") public ResponseEntity> handleCallback(@Valid @RequestBody OAuth2CallbackRequestDto requestDto) { OAuth2CallbackResponseDto responseDto = pkceService.handleCallback(requestDto); - return ResponseEntity.ok(ApiResponse.success(responseDto)); + return ResponseEntity.ok(ApiResponse.ok(responseDto)); } } diff --git a/src/main/java/apptive/devlog/domain/oauth2/dto/OAuth2CallbackRequestDto.java b/src/main/java/apptive/devlog/domain/oauth2/dto/OAuth2CallbackRequestDto.java index ad1814a..c20e88d 100644 --- a/src/main/java/apptive/devlog/domain/oauth2/dto/OAuth2CallbackRequestDto.java +++ b/src/main/java/apptive/devlog/domain/oauth2/dto/OAuth2CallbackRequestDto.java @@ -1,12 +1,18 @@ package apptive.devlog.domain.oauth2.dto; import jakarta.validation.constraints.NotBlank; -import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; -@Data +@Getter +@NoArgsConstructor +@AllArgsConstructor public class OAuth2CallbackRequestDto { @NotBlank private String code; @NotBlank private String state; + @NotBlank + private String provider; } diff --git a/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2FailureHandler.java b/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2FailureHandler.java index e58b063..31dce41 100644 --- a/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2FailureHandler.java +++ b/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2FailureHandler.java @@ -4,31 +4,40 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import java.io.IOException; -import java.util.HashMap; +import java.nio.charset.StandardCharsets; import java.util.Map; -@RequiredArgsConstructor @Component +@RequiredArgsConstructor public class OAuth2FailureHandler implements AuthenticationFailureHandler { + private final ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { - Map errorResponse = new HashMap<>(); - errorResponse.put("success", false); - errorResponse.put("error", "OAuth2 인증 실패"); - errorResponse.put("message", exception.getMessage()); + setResponseHeader(response, HttpStatus.UNAUTHORIZED); + writeErrorResponse(response, buildErrorResponse(exception)); + } - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); + private void setResponseHeader(HttpServletResponse response, HttpStatus status) { + response.setStatus(status.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + } + + private Map buildErrorResponse(AuthenticationException exception) { + return Map.of("success", false, "error", "OAuth2 인증 실패", "message", exception.getMessage()); + } - String responseBody = objectMapper.writeValueAsString(errorResponse); - response.getWriter().write(responseBody); + private void writeErrorResponse(HttpServletResponse response, Map body) throws IOException { + String json = objectMapper.writeValueAsString(body); + response.getWriter().write(json); } } diff --git a/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandler.java b/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandler.java index f11cdd2..50f8a86 100644 --- a/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandler.java +++ b/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandler.java @@ -5,36 +5,40 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; import java.io.IOException; -import java.util.HashMap; +import java.nio.charset.StandardCharsets; import java.util.Map; +@Component @RequiredArgsConstructor -@Configuration public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + private final JwtTokenProvider jwtTokenProvider; private final ObjectMapper objectMapper; @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.Authentication authentication) throws IOException { + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); String email = oAuth2User.getAttribute("email"); String accessToken = jwtTokenProvider.generateAccessToken(email); String refreshToken = jwtTokenProvider.generateRefreshToken(email); - Map tokens = new HashMap<>(); - tokens.put("accessToken", accessToken); - tokens.put("refreshToken", refreshToken); + writeJsonResponse(response, Map.of("accessToken", accessToken, "refreshToken", refreshToken)); + } - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); + private void writeJsonResponse(HttpServletResponse response, Map body) throws IOException { + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - String responseBody = objectMapper.writeValueAsString(tokens); - response.getWriter().write(responseBody); + String json = objectMapper.writeValueAsString(body); + response.getWriter().write(json); } } diff --git a/src/main/java/apptive/devlog/domain/oauth2/model/OAuth2Attributes.java b/src/main/java/apptive/devlog/domain/oauth2/model/OAuth2Attributes.java index a0da543..e4da52c 100644 --- a/src/main/java/apptive/devlog/domain/oauth2/model/OAuth2Attributes.java +++ b/src/main/java/apptive/devlog/domain/oauth2/model/OAuth2Attributes.java @@ -1,10 +1,12 @@ package apptive.devlog.domain.oauth2.model; +import apptive.devlog.common.response.error.exception.InvalidOAuth2ResponseException; import apptive.devlog.domain.user.entity.User; import apptive.devlog.domain.user.enums.Provider; import apptive.devlog.domain.user.enums.Role; import lombok.Getter; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -17,26 +19,24 @@ public class OAuth2Attributes { private final String email; private final String name; - public OAuth2Attributes(String nameAttributeKey, Provider provider, - Map attributes, String email, String name) { + private OAuth2Attributes(String nameAttributeKey, Provider provider, Map attributes, String email, String name) { this.nameAttributeKey = nameAttributeKey; this.provider = provider; - // this.attributes = attributes; this.email = email; this.name = name; - this.attributes = new HashMap<>(attributes); // 임시 방편 - this.attributes.put("email", email); // 임시 방편 + + Map copied = new HashMap<>(attributes); + copied.put("email", email); + this.attributes = Collections.unmodifiableMap(copied); } - public static OAuth2Attributes of(String registrationId, String userNameAttributeName, - Map attributes) { - if ("kakao".equals(registrationId)) { - return ofKakao(attributes); - } else if ("naver".equals(registrationId)) { - return ofNaver(attributes); - } else { - return ofGoogle(userNameAttributeName, attributes); - } + public static OAuth2Attributes of(String registrationId, String userNameAttributeName, Map attributes) { + return switch (registrationId.toLowerCase()) { + case "kakao" -> ofKakao(attributes); + case "naver" -> ofNaver(attributes); + case "google" -> ofGoogle(userNameAttributeName, attributes); + default -> throw new UnsupportedOperationException(registrationId); + }; } private static OAuth2Attributes ofGoogle(String userNameAttributeName, Map attributes) { @@ -50,7 +50,7 @@ private static OAuth2Attributes ofGoogle(String userNameAttributeName, Map attributes) { - Map response = (Map) attributes.get("response"); + Map response = getMap(attributes, "response"); return new OAuth2Attributes( "id", @@ -62,9 +62,8 @@ private static OAuth2Attributes ofNaver(Map attributes) { } private static OAuth2Attributes ofKakao(Map attributes) { - Long id = ((Number) attributes.get("id")).longValue(); - Map kakaoAccount = (Map) attributes.get("kakao_account"); - Map profile = (Map) kakaoAccount.get("profile"); + Map kakaoAccount = getMap(attributes, "kakao_account"); + Map profile = getMap(kakaoAccount, "profile"); return new OAuth2Attributes( "id", @@ -75,6 +74,14 @@ private static OAuth2Attributes ofKakao(Map attributes) { ); } + private static Map getMap(Map source, String key) { + Object value = source.get(key); + if (value instanceof Map map) { + return (Map) map; + } + throw new InvalidOAuth2ResponseException(key); + } + public User toEntity() { return User.of(email, name, provider, Role.USER); } diff --git a/src/main/java/apptive/devlog/domain/oauth2/service/CustomOAuth2UserService.java b/src/main/java/apptive/devlog/domain/oauth2/service/CustomOAuth2UserService.java index 8c825f4..0ac6996 100644 --- a/src/main/java/apptive/devlog/domain/oauth2/service/CustomOAuth2UserService.java +++ b/src/main/java/apptive/devlog/domain/oauth2/service/CustomOAuth2UserService.java @@ -18,25 +18,35 @@ @RequiredArgsConstructor @Service public class CustomOAuth2UserService implements OAuth2UserService { + private final UserRepository userRepository; + private final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = delegate.loadUser(userRequest); + OAuth2Attributes attributes = extractAttributes(userRequest, oAuth2User); + User user = saveOrUpdateUser(attributes); + + return createOAuth2User(user, attributes); + } + + private OAuth2Attributes extractAttributes(OAuth2UserRequest userRequest, OAuth2User oAuth2User) { String registrationId = userRequest.getClientRegistration().getRegistrationId(); - String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); - OAuth2User oAuth2User = new DefaultOAuth2UserService().loadUser(userRequest); + String userNameAttr = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); - OAuth2Attributes oAuth2Attributes = OAuth2Attributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); - User user = saveOrUpdate(oAuth2Attributes); + return OAuth2Attributes.of(registrationId, userNameAttr, oAuth2User.getAttributes()); + } + private User saveOrUpdateUser(OAuth2Attributes attributes) { + return userRepository.findByEmail(attributes.getEmail()).orElseGet(() -> userRepository.save(attributes.toEntity())); + } + + private OAuth2User createOAuth2User(User user, OAuth2Attributes attributes) { return new DefaultOAuth2User( Collections.singleton(new SimpleGrantedAuthority(user.getRole().name())), - oAuth2Attributes.getAttributes(), - oAuth2Attributes.getNameAttributeKey() + attributes.getAttributes(), + attributes.getNameAttributeKey() ); } - - private User saveOrUpdate(OAuth2Attributes attributes) { - return userRepository.findByEmail(attributes.getEmail()).orElseGet(() -> userRepository.save(attributes.toEntity())); - } } diff --git a/src/main/java/apptive/devlog/domain/oauth2/service/OAuth2PkceService.java b/src/main/java/apptive/devlog/domain/oauth2/service/OAuth2PkceService.java index 815c4bb..8b38f1c 100644 --- a/src/main/java/apptive/devlog/domain/oauth2/service/OAuth2PkceService.java +++ b/src/main/java/apptive/devlog/domain/oauth2/service/OAuth2PkceService.java @@ -2,9 +2,9 @@ import apptive.devlog.domain.oauth2.dto.OAuth2CallbackRequestDto; import apptive.devlog.domain.oauth2.dto.OAuth2CallbackResponseDto; -import apptive.devlog.global.response.error.exception.InvalidStateFormatException; -import apptive.devlog.global.response.error.exception.OAuth2TokenRequestException; -import apptive.devlog.global.response.error.exception.OAuth2UserInfoException; +import apptive.devlog.common.response.error.exception.InvalidStateFormatException; +import apptive.devlog.common.response.error.exception.OAuth2TokenRequestException; +import apptive.devlog.common.response.error.exception.OAuth2UserInfoException; import apptive.devlog.global.security.jwt.JwtTokenProvider; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -28,17 +28,30 @@ public class OAuth2PkceService { private final WebClient webClient; @Value("${spring.security.oauth2.client.registration.google.client-id}") - private String clientId; - + private String googleClientId; @Value("${spring.security.oauth2.client.registration.google.client-secret}") - private String clientSecret; - + private String googleClientSecret; @Value("${spring.security.oauth2.client.registration.google.redirect-uri}") - private String redirectUri; + private String googleRedirectUri; + + @Value("${spring.security.oauth2.client.registration.kakao.client-id}") + private String kakaoClientId; + @Value("${spring.security.oauth2.client.registration.kakao.client-secret}") + private String kakaoClientSecret; + @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") + private String kakaoRedirectUri; + + @Value("${spring.security.oauth2.client.registration.naver.client-id}") + private String naverClientId; + @Value("${spring.security.oauth2.client.registration.naver.client-secret}") + private String naverClientSecret; + @Value("${spring.security.oauth2.client.registration.naver.redirect-uri}") + private String naverRedirectUri; public OAuth2CallbackResponseDto handleCallback(OAuth2CallbackRequestDto requestDto) { String code = requestDto.getCode(); String state = requestDto.getState(); + String provider = requestDto.getProvider(); String[] parts = state != null ? state.split("::") : new String[0]; if (parts.length != 2) { @@ -47,15 +60,13 @@ public OAuth2CallbackResponseDto handleCallback(OAuth2CallbackRequestDto request String deviceId = parts[0]; String codeVerifier = parts[1]; - Map tokenResponse = requestAccessToken(code, codeVerifier); - String googleAccessToken = Optional.ofNullable(tokenResponse.get("access_token")) + Map tokenResponse = requestAccessToken(provider, code, codeVerifier); + String accessTokenFromProvider = Optional.ofNullable(tokenResponse.get("access_token")) .map(Object::toString) .orElseThrow(() -> new OAuth2TokenRequestException("Access token not found")); - Map userInfo = fetchGoogleUserInfo(googleAccessToken); - String email = Optional.ofNullable(userInfo.get("email")) - .map(Object::toString) - .orElseThrow(() -> new OAuth2UserInfoException("Email not found in user info")); + Map userInfo = fetchUserInfo(provider, accessTokenFromProvider); + String email = extractEmail(provider, userInfo); String accessToken = jwtTokenProvider.generateAccessToken(email); String refreshToken = jwtTokenProvider.generateRefreshToken(email); @@ -63,38 +74,89 @@ public OAuth2CallbackResponseDto handleCallback(OAuth2CallbackRequestDto request return new OAuth2CallbackResponseDto(accessToken, refreshToken); } - private Map requestAccessToken(String code, String codeVerifier) { + private Map requestAccessToken(String provider, String code, String codeVerifier) { MultiValueMap tokenRequest = new LinkedMultiValueMap<>(); - tokenRequest.add("client_id", clientId); - tokenRequest.add("client_secret", clientSecret); - tokenRequest.add("grant_type", "authorization_code"); - tokenRequest.add("code", code); - tokenRequest.add("redirect_uri", redirectUri); - tokenRequest.add("code_verifier", codeVerifier); + String tokenUri; + switch (provider.toLowerCase()) { + case "google" -> { + tokenRequest.add("client_id", googleClientId); + tokenRequest.add("client_secret", googleClientSecret); + tokenRequest.add("grant_type", "authorization_code"); + tokenRequest.add("code", code); + tokenRequest.add("redirect_uri", googleRedirectUri); + tokenRequest.add("code_verifier", codeVerifier); + tokenUri = "https://oauth2.googleapis.com/token"; + } + case "kakao" -> { + tokenRequest.add("grant_type", "authorization_code"); + tokenRequest.add("client_id", kakaoClientId); + tokenRequest.add("client_secret", kakaoClientSecret); + tokenRequest.add("code", code); + tokenRequest.add("redirect_uri", kakaoRedirectUri); + tokenUri = "https://kauth.kakao.com/oauth/token"; + } + case "naver" -> { + tokenRequest.add("grant_type", "authorization_code"); + tokenRequest.add("client_id", naverClientId); + tokenRequest.add("client_secret", naverClientSecret); + tokenRequest.add("code", code); + tokenRequest.add("redirect_uri", naverRedirectUri); + tokenUri = "https://nid.naver.com/oauth2.0/token"; + } + default -> throw new OAuth2TokenRequestException("Unsupported OAuth2 provider: " + provider); + } try { return webClient.post() - .uri("https://oauth2.googleapis.com/token") + .uri(tokenUri) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .bodyValue(tokenRequest) .retrieve() .bodyToMono(new ParameterizedTypeReference>() {}) .block(); } catch (WebClientResponseException ex) { - throw new OAuth2TokenRequestException("Failed to get token from Google: " + ex.getResponseBodyAsString()); + throw new OAuth2TokenRequestException("Failed to get token from " + provider + ": " + ex.getResponseBodyAsString()); } } - private Map fetchGoogleUserInfo(String accessToken) { + private Map fetchUserInfo(String provider, String accessToken) { + String userInfoUri = switch (provider.toLowerCase()) { + case "google" -> "https://www.googleapis.com/oauth2/v3/userinfo"; + case "kakao" -> "https://kapi.kakao.com/v2/user/me"; + case "naver" -> "https://openapi.naver.com/v1/nid/me"; + default -> throw new OAuth2UserInfoException("Unsupported OAuth2 provider: " + provider); + }; + try { return webClient.get() - .uri("https://www.googleapis.com/oauth2/v3/userinfo") + .uri(userInfoUri) .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .retrieve() .bodyToMono(new ParameterizedTypeReference>() {}) .block(); } catch (WebClientResponseException ex) { - throw new OAuth2UserInfoException("Failed to get user info from Google: " + ex.getResponseBodyAsString()); + throw new OAuth2UserInfoException("Failed to get user info from " + provider + ": " + ex.getResponseBodyAsString()); } } + + private String extractEmail(String provider, Map userInfo) { + return switch (provider.toLowerCase()) { + case "google" -> Optional.ofNullable(userInfo.get("email")) + .map(Object::toString) + .orElseThrow(() -> new OAuth2UserInfoException("Email not found in Google user info")); + case "kakao" -> { + Map kakaoAccount = (Map) userInfo.get("kakao_account"); + yield Optional.ofNullable(kakaoAccount.get("email")) + .map(Object::toString) + .orElseThrow(() -> new OAuth2UserInfoException("Email not found in Kakao user info")); + } + case "naver" -> { + Map response = (Map) userInfo.get("response"); + yield Optional.ofNullable(response.get("email")) + .map(Object::toString) + .orElseThrow(() -> new OAuth2UserInfoException("Email not found in Naver user info")); + } + default -> throw new OAuth2UserInfoException("Unsupported OAuth2 provider: " + provider); + }; + } } diff --git a/src/main/java/apptive/devlog/domain/user/controller/UserController.java b/src/main/java/apptive/devlog/domain/user/controller/UserController.java index 492ce0c..ffd0919 100644 --- a/src/main/java/apptive/devlog/domain/user/controller/UserController.java +++ b/src/main/java/apptive/devlog/domain/user/controller/UserController.java @@ -1,25 +1,26 @@ package apptive.devlog.domain.user.controller; -import apptive.devlog.global.annotation.InjectEmail; +import apptive.devlog.common.response.api.ApiResponse; import apptive.devlog.domain.user.dto.UserProfileRequestDto; import apptive.devlog.domain.user.dto.UserProfileResponseDto; import apptive.devlog.domain.user.service.UserService; -import apptive.devlog.global.response.api.ApiResponse; +import apptive.devlog.global.annotation.InjectEmail; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/user") @RequiredArgsConstructor public class UserController { + private final UserService userService; @GetMapping("/profile") public ResponseEntity> getUserProfile(@Valid @InjectEmail UserProfileRequestDto requestDto) { - return ResponseEntity.ok(ApiResponse.success(userService.getUserProfile(requestDto))); + UserProfileResponseDto responseDto = userService.getUserProfile(requestDto); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(responseDto)); } } diff --git a/src/main/java/apptive/devlog/domain/user/service/UserService.java b/src/main/java/apptive/devlog/domain/user/service/UserService.java index 7517ff2..6a2095c 100644 --- a/src/main/java/apptive/devlog/domain/user/service/UserService.java +++ b/src/main/java/apptive/devlog/domain/user/service/UserService.java @@ -4,7 +4,7 @@ import apptive.devlog.domain.user.dto.UserProfileResponseDto; import apptive.devlog.domain.user.entity.User; import apptive.devlog.domain.user.repository.UserRepository; -import apptive.devlog.global.response.error.exception.UserNotFoundException; +import apptive.devlog.common.response.error.exception.UserNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -14,8 +14,7 @@ public class UserService { private final UserRepository userRepository; public UserProfileResponseDto getUserProfile(UserProfileRequestDto requestDto) { - User user = userRepository.findByEmail(requestDto.getEmail()) - .orElseThrow(UserNotFoundException::new); + User user = userRepository.findByEmail(requestDto.getEmail()).orElseThrow(UserNotFoundException::new); return new UserProfileResponseDto(user); } } diff --git a/src/main/java/apptive/devlog/global/resolver/InjectEmailArgumentResolver.java b/src/main/java/apptive/devlog/global/resolver/InjectEmailArgumentResolver.java index fcf8b7b..5ec5fdc 100644 --- a/src/main/java/apptive/devlog/global/resolver/InjectEmailArgumentResolver.java +++ b/src/main/java/apptive/devlog/global/resolver/InjectEmailArgumentResolver.java @@ -1,8 +1,9 @@ package apptive.devlog.global.resolver; +import apptive.devlog.common.response.error.exception.DtoBindingFailedException; import apptive.devlog.global.annotation.InjectEmail; -import apptive.devlog.global.response.error.exception.TokenInjectionFailedException; -import apptive.devlog.global.response.error.exception.UnauthenticatedUserException; +import apptive.devlog.common.response.error.exception.TokenInjectionFailedException; +import apptive.devlog.common.response.error.exception.UnauthenticatedUserException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -16,6 +17,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import java.io.IOException; import java.lang.reflect.Field; @Slf4j @@ -37,32 +39,34 @@ public Object resolveArgument(MethodParameter parameter, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + Object dto = createDtoInstance(request, parameter.getParameterType()); + String email = extractCurrentUserEmail(); - String email = getCurrentUserEmail(); - - Class parameterType = parameter.getParameterType(); - Object dto; + injectFieldIfPresent(dto, "email", email); + return dto; + } - if ("POST".equalsIgnoreCase(request.getMethod()) || "PUT".equalsIgnoreCase(request.getMethod())) { - dto = objectMapper.readValue(request.getInputStream(), parameterType); - } else { - dto = parameterType.getDeclaredConstructor().newInstance(); + private Object createDtoInstance(HttpServletRequest request, Class parameterType) throws Exception { + String method = request.getMethod().toUpperCase(); + try { + if ("POST".equals(method) || "PUT".equals(method)) { + return objectMapper.readValue(request.getInputStream(), parameterType); + } + return parameterType.getDeclaredConstructor().newInstance(); + } catch (IOException e) { + throw new DtoBindingFailedException("[InjectEmailArgumentResolver] DTO 변환 실패", e); } - - injectIfFieldExists(dto, "email", email); - - return dto; } - private String getCurrentUserEmail() { + private String extractCurrentUserEmail() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) { - throw new UnauthenticatedUserException(); + throw new UnauthenticatedUserException("현재 인증된 사용자가 없습니다."); } return authentication.getName(); } - private void injectIfFieldExists(Object target, String fieldName, String value) { + private void injectFieldIfPresent(Object target, String fieldName, String value) { try { Field field = target.getClass().getDeclaredField(fieldName); if (field.getType().equals(String.class)) { @@ -70,8 +74,9 @@ private void injectIfFieldExists(Object target, String fieldName, String value) field.set(target, value); } } catch (NoSuchFieldException ignored) { + log.debug("필드 '{}' 가 존재하지 않아 주입하지 않았습니다.", fieldName); } catch (IllegalAccessException e) { - throw new TokenInjectionFailedException(fieldName, e); + throw new TokenInjectionFailedException("필드 주입 실패: " + fieldName, e); } } } diff --git a/src/main/java/apptive/devlog/global/resolver/InjectTokenArgumentResolver.java b/src/main/java/apptive/devlog/global/resolver/InjectTokenArgumentResolver.java index 9000441..361f0f7 100644 --- a/src/main/java/apptive/devlog/global/resolver/InjectTokenArgumentResolver.java +++ b/src/main/java/apptive/devlog/global/resolver/InjectTokenArgumentResolver.java @@ -1,8 +1,8 @@ package apptive.devlog.global.resolver; import apptive.devlog.global.annotation.InjectToken; -import apptive.devlog.global.response.error.exception.InvalidTokenException; -import apptive.devlog.global.response.error.exception.TokenInjectionFailedException; +import apptive.devlog.common.response.error.exception.InvalidTokenException; +import apptive.devlog.common.response.error.exception.TokenInjectionFailedException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -15,6 +15,9 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Field; @Slf4j @@ -35,32 +38,55 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); - Object dto = objectMapper.readValue(request.getInputStream(), parameter.getParameterType()); + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); - String accessToken = request.getHeader("Authorization"); - if (accessToken != null && accessToken.startsWith("Bearer ")) { - accessToken = accessToken.substring(7); - } else { - throw new InvalidTokenException(); + byte[] requestBodyBytes = getRequestBodyAsBytes(request); + Object dto; + try { + dto = objectMapper.readValue(requestBodyBytes, parameter.getParameterType()); + } catch (IOException e) { + log.error("DTO 역직렬화 실패: {}", e.getMessage(), e); + throw new TokenInjectionFailedException("DTO 역직렬화 실패", e); } - injectIfFieldExists(dto, "accessToken", accessToken); + String accessToken = extractAccessToken(request); + injectFieldIfExists(dto, "accessToken", accessToken); return dto; } - private void injectIfFieldExists(Object target, String fieldName, String value) { + private String extractAccessToken(HttpServletRequest request) { + String header = request.getHeader("Authorization"); + if (header != null && header.startsWith("Bearer ")) { + return header.substring(7); + } + throw new InvalidTokenException(); + } + + private void injectFieldIfExists(Object target, String fieldName, String value) { try { Field field = target.getClass().getDeclaredField(fieldName); if (field.getType().equals(String.class)) { field.setAccessible(true); field.set(target, value); + log.debug("필드 '{}' 에 accessToken 주입 성공", fieldName); } - } catch (NoSuchFieldException ignored) { + } catch (NoSuchFieldException e) { + log.warn("DTO에 '{}' 필드가 존재하지 않음: {}", fieldName, target.getClass().getSimpleName()); } catch (IllegalAccessException e) { throw new TokenInjectionFailedException(fieldName, e); } } + + private byte[] getRequestBodyAsBytes(HttpServletRequest request) throws IOException { + InputStream inputStream = request.getInputStream(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] data = new byte[4096]; + int nRead; + while ((nRead = inputStream.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + return buffer.toByteArray(); + } } diff --git a/src/main/java/apptive/devlog/global/response/api/ApiResponse.java b/src/main/java/apptive/devlog/global/response/api/ApiResponse.java deleted file mode 100644 index ca3e052..0000000 --- a/src/main/java/apptive/devlog/global/response/api/ApiResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package apptive.devlog.global.response.api; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class ApiResponse { - private final int status; - private final String message; - private final T data; - - public static ApiResponse success(T data) { - return new ApiResponse<>(200, "요청이 성공했습니다.", data); - } - - public static ApiResponse success(String message, T data) { - return new ApiResponse<>(200, message, data); - } -} - diff --git a/src/main/java/apptive/devlog/global/response/error/exception/TokenInjectionFailedException.java b/src/main/java/apptive/devlog/global/response/error/exception/TokenInjectionFailedException.java deleted file mode 100644 index 9812068..0000000 --- a/src/main/java/apptive/devlog/global/response/error/exception/TokenInjectionFailedException.java +++ /dev/null @@ -1,9 +0,0 @@ -package apptive.devlog.global.response.error.exception; - -import apptive.devlog.global.response.error.model.ErrorCode; - -public class TokenInjectionFailedException extends CustomException { - public TokenInjectionFailedException(String fieldName, Throwable cause) { - super(ErrorCode.TOKEN_INJECTION_FAILED, cause); - } -} diff --git a/src/main/java/apptive/devlog/global/response/error/exception/UnauthenticatedUserException.java b/src/main/java/apptive/devlog/global/response/error/exception/UnauthenticatedUserException.java deleted file mode 100644 index b9f6ee3..0000000 --- a/src/main/java/apptive/devlog/global/response/error/exception/UnauthenticatedUserException.java +++ /dev/null @@ -1,9 +0,0 @@ -package apptive.devlog.global.response.error.exception; - -import apptive.devlog.global.response.error.model.ErrorCode; - -public class UnauthenticatedUserException extends CustomException { - public UnauthenticatedUserException() { - super(ErrorCode.UNAUTHENTICATED_USER); - } -} \ No newline at end of file diff --git a/src/main/java/apptive/devlog/global/response/error/handler/GlobalExceptionHandler.java b/src/main/java/apptive/devlog/global/response/error/handler/GlobalExceptionHandler.java deleted file mode 100644 index 1e23abf..0000000 --- a/src/main/java/apptive/devlog/global/response/error/handler/GlobalExceptionHandler.java +++ /dev/null @@ -1,72 +0,0 @@ -package apptive.devlog.global.response.error.handler; - -import apptive.devlog.global.response.error.exception.*; -import apptive.devlog.global.response.error.model.ErrorCode; -import apptive.devlog.global.response.error.model.ErrorResponse; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -@RestControllerAdvice -public class GlobalExceptionHandler { - - private ResponseEntity buildResponse(ErrorCode errorCode) { - return ResponseEntity - .status(errorCode.getStatus()) - .body(new ErrorResponse( - errorCode.getStatus(), - errorCode.getMessage(), - errorCode.getCode())); - } - - @ExceptionHandler(DuplicateEmailException.class) - public ResponseEntity handleDuplicateEmail(DuplicateEmailException ex) { - return buildResponse(ErrorCode.DUPLICATE_EMAIL); - } - - @ExceptionHandler(InvalidPasswordException.class) - public ResponseEntity handleInvalidPassword(InvalidPasswordException ex) { - return buildResponse(ErrorCode.INVALID_PASSWORD); - } - - @ExceptionHandler(InvalidProviderException.class) - public ResponseEntity handleInvalidProvider(InvalidProviderException ex) { - return buildResponse(ErrorCode.INVALID_PROVIDER); - } - - @ExceptionHandler(InvalidStateFormatException.class) - public ResponseEntity handleInvalidState(InvalidStateFormatException ex) { - return buildResponse(ErrorCode.INVALID_STATE_FORMAT); - } - - @ExceptionHandler(InvalidRefreshTokenException.class) - public ResponseEntity handleInvalidRefreshToken(InvalidRefreshTokenException ex) { - return buildResponse(ErrorCode.INVALID_REFRESH_TOKEN); - } - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgument(IllegalArgumentException ex) { - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - ex.getMessage(), - "BAD_REQUEST")); - } - - @ExceptionHandler(RuntimeException.class) - public ResponseEntity handleRuntimeException(RuntimeException ex) { - return ResponseEntity - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(new ErrorResponse( - HttpStatus.INTERNAL_SERVER_ERROR.value(), - ex.getMessage(), - "INTERNAL_ERROR")); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity handleGeneral(Exception ex) { - return buildResponse(ErrorCode.UNKNOWN_ERROR); - } -} diff --git a/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java b/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java index d13ff07..0d94ef7 100644 --- a/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java +++ b/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java @@ -29,25 +29,26 @@ public class SecurityConfig { private final JwtTokenProvider jwtTokenProvider; private final CustomUserDetailsService customUserDetailsService; - private final HttpSession httpSession; - // private final CustomOAuth2AuthorizationRequestResolver customOAuth2AuthorizationRequestResolver; - // private final CustomAuthorizationCodeTokenResponseClient customAuthorizationCodeTokenResponseClient; private final CustomOAuth2UserService customOAuth2UserService; private final OAuth2SuccessHandler oAuth2SuccessHandler; private final OAuth2FailureHandler oAuth2FailureHandler; + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Bean public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsService); authenticationProvider.setPasswordEncoder(passwordEncoder); - return new ProviderManager(authenticationProvider); } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { - return new JwtAuthenticationFilter(jwtTokenProvider, customUserDetailsService, httpSession); + return new JwtAuthenticationFilter(jwtTokenProvider, customUserDetailsService); } @Bean @@ -56,16 +57,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .csrf(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - .requestMatchers("/", "/auth/signup", "/auth/login", "/auth/refresh").permitAll() - .requestMatchers("/oauth2/**").permitAll() + .requestMatchers("/", "/auth/signup", "/auth/login", "/auth/refresh", "/oauth2/**").permitAll() .requestMatchers("/auth/logout", "/user/profile").hasRole("USER") .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 - // .loginPage("/login") - .authorizationEndpoint(endpoint -> endpoint.baseUri("/oauth2/authorization")/* .authorizationRequestResolver(customOAuth2AuthorizationRequestResolver) */) + .authorizationEndpoint(endpoint -> endpoint.baseUri("/oauth2/authorization")) .redirectionEndpoint(endpoint -> endpoint.baseUri("/oauth2/callback/*")) - // .tokenEndpoint().accessTokenResponseClient(customAuthorizationCodeTokenResponseClient) .userInfoEndpoint(endpoint -> endpoint.userService(customOAuth2UserService)) .successHandler(oAuth2SuccessHandler) .failureHandler(oAuth2FailureHandler) @@ -73,19 +71,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .build(); } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } } - -/* - * http://localhost:8080/oauth2/authorization/google - * https://accounts.google.com/o/oauth2/v2/auth - * http://localhost:8080/login/oauth2/code/google - * https://oauth2.googleapis.com/token - * https://www.googleapis.com/oauth2/v3/userinfo - * CustomOAuth2UserService - * OAuth2AuthenticationSuccessHandler - */ diff --git a/src/main/java/apptive/devlog/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/apptive/devlog/global/security/filter/JwtAuthenticationFilter.java index 53dfdc6..de72bbf 100644 --- a/src/main/java/apptive/devlog/global/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/apptive/devlog/global/security/filter/JwtAuthenticationFilter.java @@ -8,6 +8,7 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; @@ -15,25 +16,31 @@ import java.io.IOException; +@Slf4j @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; private final CustomUserDetailsService customUserDetailsService; - private final HttpSession httpSession; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = resolveToken(request); if (token != null && jwtTokenProvider.validateAccessToken(token)) { - String email = jwtTokenProvider.getEmailFromToken(token); - UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(authenticationToken); - httpSession.setAttribute("email", email); + authenticate(token); + } else { + log.debug("No valid JWT token found for request to {}", request.getRequestURI()); } filterChain.doFilter(request, response); } + private void authenticate(String token) { + String email = jwtTokenProvider.getEmailFromToken(token); + UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + log.debug("Authenticated user: {}", email); + } + private String resolveToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { diff --git a/src/main/java/apptive/devlog/global/security/jwt/JwtTokenProvider.java b/src/main/java/apptive/devlog/global/security/jwt/JwtTokenProvider.java index 5db3cb1..a004ec6 100644 --- a/src/main/java/apptive/devlog/global/security/jwt/JwtTokenProvider.java +++ b/src/main/java/apptive/devlog/global/security/jwt/JwtTokenProvider.java @@ -1,14 +1,15 @@ package apptive.devlog.global.security.jwt; import apptive.devlog.infrastructure.redis.repository.RedisRepository; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.security.Key; +import java.time.Instant; +import java.util.Date; @Slf4j @Component @@ -17,6 +18,7 @@ public class JwtTokenProvider { private final long accessTokenExpiration; private final long refreshTokenExpiration; private final RedisRepository redisRepository; + private final JwtParser jwtParser; public JwtTokenProvider( @Value("${jwt.secret}") String secretKey, @@ -27,13 +29,15 @@ public JwtTokenProvider( this.accessTokenExpiration = accessTokenExpiration; this.refreshTokenExpiration = refreshTokenExpiration; this.redisRepository = redisRepository; + this.jwtParser = Jwts.parserBuilder().setSigningKey(key).build(); } public String generateToken(String email, long expiration) { + Instant now = Instant.now(); return Jwts.builder() .setSubject(email) - .setIssuedAt(java.util.Date.from(java.time.Instant.now())) - .setExpiration(java.util.Date.from(java.time.Instant.now().plusMillis(expiration))) + .setIssuedAt(Date.from(now)) + .setExpiration(Date.from(now.plusSeconds(expiration))) .signWith(key, SignatureAlgorithm.HS256) .compact(); } @@ -52,24 +56,29 @@ public String generateRefreshToken(String email) { public boolean validateToken(String token) { try { - Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + jwtParser.parseClaimsJws(token); return true; - } catch (Exception e) { - return false; + } catch (ExpiredJwtException e) { + log.warn("Token expired: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + log.warn("Unsupported token: {}", e.getMessage()); + } catch (MalformedJwtException e) { + log.warn("Malformed token: {}", e.getMessage()); + } catch (SecurityException | IllegalArgumentException e) { + log.warn("Invalid token: {}", e.getMessage()); } + return false; } public boolean validateAccessToken(String token) { - if (!validateToken(token)) return false; - return redisRepository.hasAccessToken(token); + return validateToken(token) && redisRepository.hasAccessToken(token); } public boolean validateRefreshToken(String token) { - if (!validateToken(token)) return false; - return redisRepository.hasRefreshToken(token); + return validateToken(token) && redisRepository.hasRefreshToken(token); } public String getEmailFromToken(String token) { - return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getSubject(); + return jwtParser.parseClaimsJws(token).getBody().getSubject(); } } diff --git a/src/main/java/apptive/devlog/global/security/model/CustomUserDetails.java b/src/main/java/apptive/devlog/global/security/model/CustomUserDetails.java new file mode 100644 index 0000000..3ca3f59 --- /dev/null +++ b/src/main/java/apptive/devlog/global/security/model/CustomUserDetails.java @@ -0,0 +1,53 @@ +package apptive.devlog.global.security.model; + +import apptive.devlog.domain.user.entity.User; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +@Getter +public class CustomUserDetails implements UserDetails { + private final User user; + + public CustomUserDetails(User user) { + this.user = user; + } + + @Override + public Collection getAuthorities() { + return Collections.singleton(() -> "ROLE_" + user.getRole().name()); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/apptive/devlog/global/security/service/CustomUserDetailsService.java b/src/main/java/apptive/devlog/global/security/service/CustomUserDetailsService.java index b3bd985..30ae2db 100644 --- a/src/main/java/apptive/devlog/global/security/service/CustomUserDetailsService.java +++ b/src/main/java/apptive/devlog/global/security/service/CustomUserDetailsService.java @@ -2,6 +2,7 @@ import apptive.devlog.domain.user.entity.User; import apptive.devlog.domain.user.repository.UserRepository; +import apptive.devlog.global.security.model.CustomUserDetails; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -16,6 +17,6 @@ public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); - return org.springframework.security.core.userdetails.User.withUsername(user.getEmail()).password(user.getPassword()).roles(user.getRole().name()).build(); + return new CustomUserDetails(user); } } diff --git a/src/main/java/apptive/devlog/infrastructure/redis/config/RedisConfig.java b/src/main/java/apptive/devlog/infrastructure/redis/config/RedisConfig.java index 597b65c..3421510 100644 --- a/src/main/java/apptive/devlog/infrastructure/redis/config/RedisConfig.java +++ b/src/main/java/apptive/devlog/infrastructure/redis/config/RedisConfig.java @@ -1,5 +1,6 @@ package apptive.devlog.infrastructure.redis.config; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -9,28 +10,30 @@ @Configuration public class RedisConfig { + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + @Bean public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(); + return new LettuceConnectionFactory(redisHost, redisPort); } @Bean - public StringRedisSerializer stringRedisSerializer() { - return new StringRedisSerializer(); - } + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + StringRedisSerializer stringSerializer = new StringRedisSerializer(); - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory, StringRedisSerializer stringRedisSerializer) { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); - redisTemplate.setKeySerializer(stringRedisSerializer); - redisTemplate.setValueSerializer(stringRedisSerializer); - redisTemplate.setHashKeySerializer(stringRedisSerializer); - redisTemplate.setHashValueSerializer(stringRedisSerializer); + redisTemplate.setKeySerializer(stringSerializer); + redisTemplate.setValueSerializer(stringSerializer); + redisTemplate.setHashKeySerializer(stringSerializer); + redisTemplate.setHashValueSerializer(stringSerializer); redisTemplate.afterPropertiesSet(); - return redisTemplate; } } diff --git a/src/main/java/apptive/devlog/infrastructure/redis/repository/RedisRepository.java b/src/main/java/apptive/devlog/infrastructure/redis/repository/RedisRepository.java index 55d2c2a..dc3e530 100644 --- a/src/main/java/apptive/devlog/infrastructure/redis/repository/RedisRepository.java +++ b/src/main/java/apptive/devlog/infrastructure/redis/repository/RedisRepository.java @@ -9,10 +9,11 @@ @Repository public class RedisRepository { + private static final String ACCESS_TOKEN_PREFIX = "AT:"; + private static final String REFRESH_TOKEN_PREFIX = "RT:"; private final long accessTokenExpiration; private final long refreshTokenExpiration; private final RedisTemplate redisTemplate; - private static final Duration TTL = Duration.ofMinutes(10); public RedisRepository( @Value("${jwt.access-token-expiration}") long accessTokenExpiration, @@ -24,26 +25,26 @@ public RedisRepository( } public void saveAccessToken(String accessToken, String email) { - redisTemplate.opsForValue().set("AT:" + accessToken, email, accessTokenExpiration, TimeUnit.MILLISECONDS); + redisTemplate.opsForValue().set(ACCESS_TOKEN_PREFIX + accessToken, email, accessTokenExpiration, TimeUnit.MILLISECONDS); } - public boolean hasAccessToken(String accessToken) { - return Boolean.TRUE.equals(redisTemplate.hasKey("AT:" + accessToken)); + public void saveRefreshToken(String refreshToken, String email) { + redisTemplate.opsForValue().set(REFRESH_TOKEN_PREFIX + refreshToken, email, refreshTokenExpiration, TimeUnit.MILLISECONDS); } - public void deleteAccessToken(String accessToken) { - redisTemplate.delete("AT:" + accessToken); + public boolean hasAccessToken(String accessToken) { + return Boolean.TRUE.equals(redisTemplate.hasKey(ACCESS_TOKEN_PREFIX + accessToken)); } - public void saveRefreshToken(String refreshToken, String email) { - redisTemplate.opsForValue().set("RT:" + refreshToken, email, refreshTokenExpiration, TimeUnit.MILLISECONDS); + public boolean hasRefreshToken(String refreshToken) { + return Boolean.TRUE.equals(redisTemplate.hasKey(REFRESH_TOKEN_PREFIX + refreshToken)); } - public boolean hasRefreshToken(String refreshToken) { - return Boolean.TRUE.equals(redisTemplate.hasKey("RT:" + refreshToken)); + public void deleteAccessToken(String accessToken) { + redisTemplate.delete(ACCESS_TOKEN_PREFIX + accessToken); } public void deleteRefreshToken(String refreshToken) { - redisTemplate.delete("RT:" + refreshToken); + redisTemplate.delete(REFRESH_TOKEN_PREFIX + refreshToken); } } diff --git a/src/main/java/apptive/devlog/support/testconfig/redis/RedisInitializer.java b/src/main/java/apptive/devlog/support/testconfig/redis/RedisInitializer.java index 7a6ab05..3657ba6 100644 --- a/src/main/java/apptive/devlog/support/testconfig/redis/RedisInitializer.java +++ b/src/main/java/apptive/devlog/support/testconfig/redis/RedisInitializer.java @@ -1,18 +1,31 @@ package apptive.devlog.support.testconfig.redis; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Profile; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; +import java.util.Set; + +@Slf4j @RequiredArgsConstructor @Component +@Profile("local") public class RedisInitializer implements ApplicationRunner { private final RedisTemplate redisTemplate; @Override public void run(ApplicationArguments args) { - redisTemplate.delete(redisTemplate.keys("*")); + Set keys = redisTemplate.keys("*"); + + if (!keys.isEmpty()) { + redisTemplate.delete(keys); + log.info("Redis 초기화 완료 - {}개의 키 삭제됨", keys.size()); + } else { + log.info("Redis 초기화 생략 - 삭제할 키 없음"); + } } } diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties new file mode 100644 index 0000000..2bcfadb --- /dev/null +++ b/src/main/resources/application-local.properties @@ -0,0 +1,80 @@ +# =============================== +# DB CONFIG +# =============================== +spring.datasource.url=jdbc:mysql://localhost:3306/devlog?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.username=root + +# =============================== +# JPA CONFIG +# =============================== +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=create +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.show_sql=true +spring.jpa.open-in-view=false +spring.jpa.show-sql=true + +# =============================== +# SERVER CONFIG +# =============================== +server.port=8080 +server.ssl.enabled=false + +# =============================== +# REDIS CONFIG +# =============================== +spring.data.redis.host=localhost +spring.data.redis.port=6379 + +# =============================== +# LOGGING +# =============================== +logging.level.org.springframework.security=DEBUG +logging.level.com.apptive.devlog=DEBUG + +# =============================== +# JWT CONFIG +# =============================== +jwt.access-token-expiration=3600000 +jwt.refresh-token-expiration=1209600000 + +# =============================== +# OAUTH2 CONFIG - GOOGLE +# =============================== +spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.google.scope=profile,email +spring.security.oauth2.client.registration.google.client-name=Google +spring.security.oauth2.client.registration.google.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth +spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token +spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo +spring.security.oauth2.client.provider.google.user-name-attribute=sub + +# =============================== +# OAUTH2 CONFIG - NAVER +# =============================== +spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.naver.scope=name,email +spring.security.oauth2.client.registration.naver.client-name=Naver +spring.security.oauth2.client.registration.naver.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize +spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token +spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me +spring.security.oauth2.client.provider.naver.user-name-attribute=response + +# =============================== +# OAUTH2 CONFIG - KAKAO +# =============================== +spring.security.oauth2.client.registration.kakao.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email +spring.security.oauth2.client.registration.kakao.client-name=Kakao +spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize +spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token +spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me +spring.security.oauth2.client.provider.kakao.user-name-attribute=id diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8f41cb1..3210b1a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,90 +1,10 @@ # =============================== -# SPRING CONFIG +# APPLICATION CONFIG # =============================== spring.application.name=devlog -spring.datasource.url=jdbc:mysql://localhost:3306/devlog?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 -spring.datasource.username=root -spring.datasource.password= -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # =============================== -# JPA CONFIG +# PROFILE CONFIG # =============================== -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect -spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect -spring.jpa.hibernate.ddl-auto=create -spring.jpa.properties.hibernate.format_sql=true -spring.jpa.properties.hibernate.show_sql=true -spring.jpa.open-in-view=false -spring.jpa.show-sql=true - -# =============================== -# SERVER CONFIG -# =============================== -server.port=8080 -server.ssl.enabled=false - -# =============================== -# REDIS CONFIG -# =============================== -spring.data.redis.host=localhost -spring.data.redis.port=6379 -spring.data.redis.password= - -# =============================== -# LOGGING -# =============================== -logging.level.org.springframework.security=DEBUG -logging.level.com.apptive.devlog=DEBUG - -# =============================== -# JWT CONFIG -# =============================== -jwt.secret= -jwt.access-token-expiration=3600000 -jwt.refresh-token-expiration=1209600000 - -# =============================== -# OAUTH2 CONFIG - GOOGLE -# =============================== -spring.security.oauth2.client.registration.google.client-id= -spring.security.oauth2.client.registration.google.client-secret= -spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/oauth2/callback/{registrationId} -spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code -spring.security.oauth2.client.registration.google.scope=profile,email -spring.security.oauth2.client.registration.google.client-name=Google -spring.security.oauth2.client.registration.google.client-authentication-method=client_secret_post -spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth -spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token -spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo -spring.security.oauth2.client.provider.google.user-name-attribute=sub - -# =============================== -# OAUTH2 CONFIG - NAVER -# =============================== -spring.security.oauth2.client.registration.naver.client-id= -spring.security.oauth2.client.registration.naver.client-secret= -spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/oauth2/callback/{registrationId} -spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code -spring.security.oauth2.client.registration.naver.scope=name,email -spring.security.oauth2.client.registration.naver.client-name=Naver -spring.security.oauth2.client.registration.naver.client-authentication-method=client_secret_post -spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize -spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token -spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me -spring.security.oauth2.client.provider.naver.user-name-attribute=response - -# =============================== -# OAUTH2 CONFIG - KAKAO -# =============================== -spring.security.oauth2.client.registration.kakao.client-id= -spring.security.oauth2.client.registration.kakao.client-secret= -spring.security.oauth2.client.registration.kakao.redirect-uri={baseUrl}/oauth2/callback/{registrationId} -spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code -spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email -spring.security.oauth2.client.registration.kakao.client-name=Kakao -spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post -spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize -spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token -spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me -spring.security.oauth2.client.provider.kakao.user-name-attribute=id +spring.profiles.active=local +spring.profiles.include=secret From e9f9d7899814db49ae5a02dd5c015a88261e7aac Mon Sep 17 00:00:00 2001 From: qqwwee771441 <104188251+qqwwee771441@users.noreply.github.com> Date: Wed, 9 Apr 2025 04:14:04 +0900 Subject: [PATCH 04/11] Create local.properties --- local.properties | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 local.properties diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..310516d --- /dev/null +++ b/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Wed Apr 09 04:13:29 KST 2025 +sdk.dir=C\:\\Users\\qqwwe\\AppData\\Local\\Android\\Sdk From c7bd5a5f93501b29dc7cb77025a03d5a2ad55229 Mon Sep 17 00:00:00 2001 From: qqwwee771441 <104188251+qqwwee771441@users.noreply.github.com> Date: Wed, 9 Apr 2025 05:53:49 +0900 Subject: [PATCH 05/11] feat: swagger --- build.gradle | 2 + .../common/response/api/ApiResponse.java | 96 ---------------- .../response/success/CommonResponse.java | 105 ++++++++++++++++++ .../documentation/config/SwaggerConfig.java | 35 ++++++ .../config/SwaggerGroupConfig.java | 25 +++++ .../documentation/tags/AuthDocumentation.java | 7 ++ .../documentation/tags/UserDocumentation.java | 7 ++ .../wrapper/UserLoginResponseWrapper.java | 8 ++ .../wrapper/UserRefreshResponseWrapper.java | 9 ++ .../wrapper/UserSignupResponseWrapper.java | 8 ++ .../auth/controller/AuthController.java | 66 +++++++++-- .../domain/auth/dto/UserLoginRequestDto.java | 5 + .../domain/auth/dto/UserLoginResponseDto.java | 5 + .../domain/auth/dto/UserLogoutRequestDto.java | 9 +- .../auth/dto/UserRefreshRequestDto.java | 5 + .../auth/dto/UserRefreshResponseDto.java | 5 + .../domain/auth/dto/UserSignupRequestDto.java | 10 ++ .../auth/dto/UserSignupResponseDto.java | 4 + .../controller/OAuth2PkceController.java | 6 +- .../user/controller/UserController.java | 25 ++++- .../user/dto/UserProfileRequestDto.java | 3 + .../user/dto/UserProfileResponseDto.java | 7 ++ .../security/config/SecurityConfig.java | 2 +- .../resources/application-local.properties | 8 ++ 24 files changed, 344 insertions(+), 118 deletions(-) delete mode 100644 src/main/java/apptive/devlog/common/response/api/ApiResponse.java create mode 100644 src/main/java/apptive/devlog/common/response/success/CommonResponse.java create mode 100644 src/main/java/apptive/devlog/documentation/config/SwaggerConfig.java create mode 100644 src/main/java/apptive/devlog/documentation/config/SwaggerGroupConfig.java create mode 100644 src/main/java/apptive/devlog/documentation/tags/AuthDocumentation.java create mode 100644 src/main/java/apptive/devlog/documentation/tags/UserDocumentation.java create mode 100644 src/main/java/apptive/devlog/documentation/wrapper/UserLoginResponseWrapper.java create mode 100644 src/main/java/apptive/devlog/documentation/wrapper/UserRefreshResponseWrapper.java create mode 100644 src/main/java/apptive/devlog/documentation/wrapper/UserSignupResponseWrapper.java diff --git a/build.gradle b/build.gradle index 7a21d9f..059d872 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' diff --git a/src/main/java/apptive/devlog/common/response/api/ApiResponse.java b/src/main/java/apptive/devlog/common/response/api/ApiResponse.java deleted file mode 100644 index 55bc1aa..0000000 --- a/src/main/java/apptive/devlog/common/response/api/ApiResponse.java +++ /dev/null @@ -1,96 +0,0 @@ -package apptive.devlog.common.response.api; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Builder; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -import java.time.LocalDateTime; - -@Getter -@JsonInclude(JsonInclude.Include.NON_NULL) -public class ApiResponse { - - private final boolean success; - private final int status; - private final String message; - private final T data; - private final LocalDateTime timestamp; - - @Builder - private ApiResponse(boolean success, HttpStatus httpStatus, String message, T data, LocalDateTime timestamp) { - this.success = success; - this.status = httpStatus.value(); - this.message = message; - this.data = data; - this.timestamp = timestamp != null ? timestamp : LocalDateTime.now(); - } - - // ===== Success ===== - - public static ApiResponse success(HttpStatus status, String message, T data) { - return ApiResponse.builder() - .success(true) - .httpStatus(status) - .message(message) - .data(data) - .build(); - } - - public static ApiResponse success(HttpStatus status, T data) { - return ApiResponse.builder() - .success(true) - .httpStatus(status) - .message("요청이 성공했습니다.") - .data(data) - .build(); - } - - public static ApiResponse success(HttpStatus status, String message) { - return ApiResponse.builder() - .success(true) - .httpStatus(status) - .message(message) - .build(); - } - - public static ApiResponse success(HttpStatus status) { - return ApiResponse.builder() - .success(true) - .httpStatus(status) - .message("요청이 성공했습니다.") - .build(); - } - - // 기본 200 OK - public static ApiResponse ok(T data) { - return success(HttpStatus.OK, data); - } - - public static ApiResponse created(T data) { - return success(HttpStatus.CREATED, data); - } - - public static ApiResponse noContent() { - return success(HttpStatus.NO_CONTENT, (Void) null); - } - - // ===== Error ===== - - public static ApiResponse error(HttpStatus status, String message) { - return ApiResponse.builder() - .success(false) - .httpStatus(status) - .message(message) - .build(); - } - - public static ApiResponse error(HttpStatus status, String message, T data) { - return ApiResponse.builder() - .success(false) - .httpStatus(status) - .message(message) - .data(data) - .build(); - } -} diff --git a/src/main/java/apptive/devlog/common/response/success/CommonResponse.java b/src/main/java/apptive/devlog/common/response/success/CommonResponse.java new file mode 100644 index 0000000..9289049 --- /dev/null +++ b/src/main/java/apptive/devlog/common/response/success/CommonResponse.java @@ -0,0 +1,105 @@ +package apptive.devlog.common.response.success; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "API 공통 응답 포맷") +public class CommonResponse { + + @Schema(description = "요청 성공 여부", example = "true") + private boolean success; + @Schema(description = "HTTP 상태 코드", example = "200") + private int status; + @Schema(description = "응답 메시지", example = "요청이 성공했습니다.") + private String message; + @Schema(description = "응답 데이터", implementation = Object.class) + private T data; + @Schema(description = "응답 시간", example = "2024-04-08T12:34:56") + private LocalDateTime timestamp; + + @Builder + private CommonResponse(boolean success, HttpStatus httpStatus, String message, T data, LocalDateTime timestamp) { + this.success = success; + this.status = httpStatus.value(); + this.message = message; + this.data = data; + this.timestamp = timestamp != null ? timestamp : LocalDateTime.now(); + } + + // ===== Success ===== + + public static CommonResponse success(HttpStatus status, String message, T data) { + return CommonResponse.builder() + .success(true) + .httpStatus(status) + .message(message) + .data(data) + .build(); + } + + public static CommonResponse success(HttpStatus status, T data) { + return CommonResponse.builder() + .success(true) + .httpStatus(status) + .message("요청이 성공했습니다.") + .data(data) + .build(); + } + + public static CommonResponse success(HttpStatus status, String message) { + return CommonResponse.builder() + .success(true) + .httpStatus(status) + .message(message) + .build(); + } + + public static CommonResponse success(HttpStatus status) { + return CommonResponse.builder() + .success(true) + .httpStatus(status) + .message("요청이 성공했습니다.") + .build(); + } + + // 기본 200 OK + public static CommonResponse ok(T data) { + return success(HttpStatus.OK, data); + } + + public static CommonResponse created(T data) { + return success(HttpStatus.CREATED, data); + } + + public static CommonResponse noContent() { + return success(HttpStatus.NO_CONTENT, (Void) null); + } + + // ===== Error ===== + + public static CommonResponse error(HttpStatus status, String message) { + return CommonResponse.builder() + .success(false) + .httpStatus(status) + .message(message) + .build(); + } + + public static CommonResponse error(HttpStatus status, String message, T data) { + return CommonResponse.builder() + .success(false) + .httpStatus(status) + .message(message) + .data(data) + .build(); + } +} diff --git a/src/main/java/apptive/devlog/documentation/config/SwaggerConfig.java b/src/main/java/apptive/devlog/documentation/config/SwaggerConfig.java new file mode 100644 index 0000000..4e8a9ae --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/config/SwaggerConfig.java @@ -0,0 +1,35 @@ +package apptive.devlog.documentation.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + private static final String SECURITY_SCHEME_NAME = "bearerAuth"; + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("Devlog API") + .description("Devlog 프로젝트의 API 명세서입니다.") + .version("v1.0") + .contact(new Contact().name("Devlog Team").email("devlog@example.com")) + .license(new License().name("Apache 2.0").url("http://springdoc.org"))) + .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) + .components(new io.swagger.v3.oas.models.Components() + .addSecuritySchemes(SECURITY_SCHEME_NAME, + new SecurityScheme() + .name(SECURITY_SCHEME_NAME) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } +} diff --git a/src/main/java/apptive/devlog/documentation/config/SwaggerGroupConfig.java b/src/main/java/apptive/devlog/documentation/config/SwaggerGroupConfig.java new file mode 100644 index 0000000..638f15e --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/config/SwaggerGroupConfig.java @@ -0,0 +1,25 @@ +package apptive.devlog.documentation.config; + +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerGroupConfig { + + @Bean + public GroupedOpenApi publicApiV1() { + return GroupedOpenApi.builder() + .group("v1") + .pathsToMatch("/api/v1/**") + .build(); + } + + @Bean + public GroupedOpenApi publicApiV2() { + return GroupedOpenApi.builder() + .group("v2") + .pathsToMatch("/api/v2/**") + .build(); + } +} diff --git a/src/main/java/apptive/devlog/documentation/tags/AuthDocumentation.java b/src/main/java/apptive/devlog/documentation/tags/AuthDocumentation.java new file mode 100644 index 0000000..08ee36c --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/tags/AuthDocumentation.java @@ -0,0 +1,7 @@ +package apptive.devlog.documentation.tags; + +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Auth", description = "인증 관련 API (회원가입, 로그인, 로그아웃 등)") +public interface AuthDocumentation { +} diff --git a/src/main/java/apptive/devlog/documentation/tags/UserDocumentation.java b/src/main/java/apptive/devlog/documentation/tags/UserDocumentation.java new file mode 100644 index 0000000..de68904 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/tags/UserDocumentation.java @@ -0,0 +1,7 @@ +package apptive.devlog.documentation.tags; + +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "User", description = "사용자 관련 API") +public interface UserDocumentation { +} diff --git a/src/main/java/apptive/devlog/documentation/wrapper/UserLoginResponseWrapper.java b/src/main/java/apptive/devlog/documentation/wrapper/UserLoginResponseWrapper.java new file mode 100644 index 0000000..cc3c6c7 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/wrapper/UserLoginResponseWrapper.java @@ -0,0 +1,8 @@ +package apptive.devlog.documentation.wrapper; + +import apptive.devlog.common.response.success.CommonResponse; +import apptive.devlog.domain.auth.dto.UserLoginResponseDto; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "UserLoginResponseWrapper") +public class UserLoginResponseWrapper extends CommonResponse {} \ No newline at end of file diff --git a/src/main/java/apptive/devlog/documentation/wrapper/UserRefreshResponseWrapper.java b/src/main/java/apptive/devlog/documentation/wrapper/UserRefreshResponseWrapper.java new file mode 100644 index 0000000..18ce427 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/wrapper/UserRefreshResponseWrapper.java @@ -0,0 +1,9 @@ +package apptive.devlog.documentation.wrapper; + +import apptive.devlog.common.response.success.CommonResponse; +import apptive.devlog.domain.auth.dto.UserRefreshResponseDto; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "UserRefreshResponseWrapper") +public class UserRefreshResponseWrapper extends CommonResponse {} + diff --git a/src/main/java/apptive/devlog/documentation/wrapper/UserSignupResponseWrapper.java b/src/main/java/apptive/devlog/documentation/wrapper/UserSignupResponseWrapper.java new file mode 100644 index 0000000..5c43129 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/wrapper/UserSignupResponseWrapper.java @@ -0,0 +1,8 @@ +package apptive.devlog.documentation.wrapper; + +import apptive.devlog.common.response.success.CommonResponse; +import apptive.devlog.domain.auth.dto.UserSignupResponseDto; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "UserSignupResponseWrapper") +public class UserSignupResponseWrapper extends CommonResponse {} diff --git a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java index 7ad7100..e258abd 100644 --- a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java +++ b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java @@ -1,42 +1,84 @@ package apptive.devlog.domain.auth.controller; +import apptive.devlog.common.response.success.CommonResponse; +import apptive.devlog.documentation.tags.AuthDocumentation; +import apptive.devlog.documentation.wrapper.UserLoginResponseWrapper; +import apptive.devlog.documentation.wrapper.UserRefreshResponseWrapper; +import apptive.devlog.documentation.wrapper.UserSignupResponseWrapper; import apptive.devlog.domain.auth.dto.*; -import apptive.devlog.global.annotation.InjectToken; import apptive.devlog.domain.auth.service.AuthService; -import apptive.devlog.common.response.api.ApiResponse; -import jakarta.validation.Valid; +import apptive.devlog.global.annotation.InjectToken; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import jakarta.validation.Valid; + @RestController @RequestMapping("/auth") @RequiredArgsConstructor -public class AuthController { +public class AuthController implements AuthDocumentation { private final AuthService authService; + @Operation(summary = "회원 가입", description = "회원으로 가입합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "회원가입 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UserSignupResponseWrapper.class) + )), + @ApiResponse(responseCode = "400", description = "요청 파라미터 오류 또는 중복된 이메일", + content = @Content(schema = @Schema(implementation = CommonResponse.class))) + }) @PostMapping("/signup") - public ResponseEntity> signup(@Valid @RequestBody UserSignupRequestDto requestDto) { + public ResponseEntity> signup(@Valid @RequestBody UserSignupRequestDto requestDto) { UserSignupResponseDto responseDto = authService.signup(requestDto); - return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.created(responseDto)); + return ResponseEntity.status(HttpStatus.CREATED).body(CommonResponse.created(responseDto)); } + @Operation(summary = "회원 로그인", description = "이메일과 비밀번호로 로그인합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UserLoginResponseWrapper.class) + )), + @ApiResponse(responseCode = "401", description = "인증 실패", content = @Content) + }) @PostMapping("/login") - public ResponseEntity> login(@Valid @RequestBody UserLoginRequestDto requestDto) { + public ResponseEntity> login(@Valid @RequestBody UserLoginRequestDto requestDto) { UserLoginResponseDto responseDto = authService.login(requestDto); - return ResponseEntity.ok(ApiResponse.ok(responseDto)); + return ResponseEntity.ok(CommonResponse.ok(responseDto)); } + @Operation(summary = "토큰 재발급", description = "Refresh Token을 이용하여 Access Token을 재발급합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "토큰 재발급 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UserRefreshResponseWrapper.class) + )), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰", content = @Content) + }) @PostMapping("/refresh") - public ResponseEntity> refresh(@Valid @InjectToken UserRefreshRequestDto requestDto) { + public ResponseEntity> refresh(@Valid @InjectToken UserRefreshRequestDto requestDto) { UserRefreshResponseDto responseDto = authService.refresh(requestDto); - return ResponseEntity.ok(ApiResponse.ok(responseDto)); + return ResponseEntity.ok(CommonResponse.ok(responseDto)); } + @Operation(summary = "로그아웃", description = "로그아웃 처리합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "로그아웃 성공", content = @Content) + }) @PostMapping("/logout") - public ResponseEntity> logout(@Valid @InjectToken UserLogoutRequestDto requestDto) { + public ResponseEntity> logout(@Valid @InjectToken UserLogoutRequestDto requestDto) { authService.logout(requestDto); - return ResponseEntity.status(HttpStatus.NO_CONTENT).body(ApiResponse.noContent()); + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(CommonResponse.noContent()); } } diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserLoginRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserLoginRequestDto.java index 3f18dd9..417eb1b 100644 --- a/src/main/java/apptive/devlog/domain/auth/dto/UserLoginRequestDto.java +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserLoginRequestDto.java @@ -1,5 +1,6 @@ package apptive.devlog.domain.auth.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; @@ -9,10 +10,14 @@ @Getter @NoArgsConstructor @AllArgsConstructor +@Schema(description = "로그인 요청 DTO") public class UserLoginRequestDto { + @Schema(description = "사용자 이메일", example = "user@example.com", requiredMode = Schema.RequiredMode.REQUIRED) @Email @NotBlank private String email; + + @Schema(description = "사용자 비밀번호", example = "securePassword123!", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank private String password; } diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserLoginResponseDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserLoginResponseDto.java index 7bd8ccc..0752d7c 100644 --- a/src/main/java/apptive/devlog/domain/auth/dto/UserLoginResponseDto.java +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserLoginResponseDto.java @@ -1,11 +1,16 @@ package apptive.devlog.domain.auth.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor +@Schema(description = "로그인 응답 DTO") public class UserLoginResponseDto { + @Schema(description = "엑세스 토큰", example = "eyJhbGciOiJIUzI1...") private String accessToken; + + @Schema(description = "리프레시 토큰", example = "eyJhbGciOiJIUzI1...") private String refreshToken; } diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserLogoutRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserLogoutRequestDto.java index 7d7c343..aebbaad 100644 --- a/src/main/java/apptive/devlog/domain/auth/dto/UserLogoutRequestDto.java +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserLogoutRequestDto.java @@ -1,14 +1,19 @@ package apptive.devlog.domain.auth.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor +@Schema(description = "사용자 로그아웃 요청 DTO") public class UserLogoutRequestDto { - @NotBlank + @Schema(description = "Access Token", example = "eyJhbGciOiJIUzI1NiJ9...") + @NotBlank(message = "Access Token은 필수입니다.") private String accessToken; - @NotBlank + + @Schema(description = "Refresh Token", example = "dGhpc2lzYXJlZnJlc2h0b2tlbg==") + @NotBlank(message = "Refresh Token은 필수입니다.") private String refreshToken; } diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshRequestDto.java index ae295e7..b646862 100644 --- a/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshRequestDto.java +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshRequestDto.java @@ -1,14 +1,19 @@ package apptive.devlog.domain.auth.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor +@Schema(description = "Access Token과 Refresh Token 재발급 요청 DTO") public class UserRefreshRequestDto { @NotBlank + @Schema(description = "기존 Access Token", example = "eyJhbGciOiJIUzI1NiIsInR...") private String accessToken; + @NotBlank + @Schema(description = "Refresh Token", example = "dGhpc2lzYXJlZnJlc2h0b2tlbg==") private String refreshToken; } diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshResponseDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshResponseDto.java index 06b0f1a..9132266 100644 --- a/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshResponseDto.java +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshResponseDto.java @@ -1,11 +1,16 @@ package apptive.devlog.domain.auth.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor +@Schema(description = "액세스/리프레시 토큰 응답 DTO") public class UserRefreshResponseDto { + @Schema(description = "Access Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") private String accessToken; + + @Schema(description = "Refresh Token", example = "dGhpc19pc19hX3JlZnJlc2hfdG9rZW4=") private String refreshToken; } \ No newline at end of file diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserSignupRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserSignupRequestDto.java index 48537fc..f509029 100644 --- a/src/main/java/apptive/devlog/domain/auth/dto/UserSignupRequestDto.java +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserSignupRequestDto.java @@ -4,35 +4,45 @@ import apptive.devlog.domain.user.enums.Gender; import apptive.devlog.domain.user.enums.Provider; import apptive.devlog.domain.user.enums.Role; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import java.time.LocalDate; +@Builder @Getter @NoArgsConstructor @AllArgsConstructor +@Schema(description = "회원가입 요청 DTO") public class UserSignupRequestDto { + @Schema(description = "이메일", example = "test@example.com", requiredMode = Schema.RequiredMode.REQUIRED) @Email @NotBlank private String email; + @Schema(description = "비밀번호 (8자 이상 20자 이하)", example = "P@ssword123", requiredMode = Schema.RequiredMode.REQUIRED) @Size(min = 8, max = 20) @NotBlank private String password; + @Schema(description = "이름", example = "홍길동", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank private String name; + @Schema(description = "닉네임", example = "길동이") private String nickname; + @Schema(description = "생년월일", example = "1990-01-01") private LocalDate birth; + @Schema(description = "성별", example = "MALE") private Gender gender; public User toEntity(PasswordEncoder passwordEncoder) { diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserSignupResponseDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserSignupResponseDto.java index 35ccb06..c2f1d55 100644 --- a/src/main/java/apptive/devlog/domain/auth/dto/UserSignupResponseDto.java +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserSignupResponseDto.java @@ -1,10 +1,14 @@ package apptive.devlog.domain.auth.dto; import apptive.devlog.domain.user.entity.User; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; import lombok.Getter; +@Schema(description = "회원가입 결과 응답") @Getter public class UserSignupResponseDto { + @Schema(description = "가입된 유저의 이메일", example = "test@example.com") private final String email; public UserSignupResponseDto(User user) { diff --git a/src/main/java/apptive/devlog/domain/oauth2/controller/OAuth2PkceController.java b/src/main/java/apptive/devlog/domain/oauth2/controller/OAuth2PkceController.java index d0708af..218a80a 100644 --- a/src/main/java/apptive/devlog/domain/oauth2/controller/OAuth2PkceController.java +++ b/src/main/java/apptive/devlog/domain/oauth2/controller/OAuth2PkceController.java @@ -3,7 +3,7 @@ import apptive.devlog.domain.oauth2.dto.OAuth2CallbackRequestDto; import apptive.devlog.domain.oauth2.dto.OAuth2CallbackResponseDto; import apptive.devlog.domain.oauth2.service.OAuth2PkceService; -import apptive.devlog.common.response.api.ApiResponse; +import apptive.devlog.common.response.success.CommonResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -17,8 +17,8 @@ public class OAuth2PkceController { private final OAuth2PkceService pkceService; @PostMapping("/pkce/callback") - public ResponseEntity> handleCallback(@Valid @RequestBody OAuth2CallbackRequestDto requestDto) { + public ResponseEntity> handleCallback(@Valid @RequestBody OAuth2CallbackRequestDto requestDto) { OAuth2CallbackResponseDto responseDto = pkceService.handleCallback(requestDto); - return ResponseEntity.ok(ApiResponse.ok(responseDto)); + return ResponseEntity.ok(CommonResponse.ok(responseDto)); } } diff --git a/src/main/java/apptive/devlog/domain/user/controller/UserController.java b/src/main/java/apptive/devlog/domain/user/controller/UserController.java index ffd0919..8e4cf16 100644 --- a/src/main/java/apptive/devlog/domain/user/controller/UserController.java +++ b/src/main/java/apptive/devlog/domain/user/controller/UserController.java @@ -1,26 +1,43 @@ package apptive.devlog.domain.user.controller; -import apptive.devlog.common.response.api.ApiResponse; +import apptive.devlog.common.response.success.CommonResponse; +import apptive.devlog.documentation.tags.UserDocumentation; import apptive.devlog.domain.user.dto.UserProfileRequestDto; import apptive.devlog.domain.user.dto.UserProfileResponseDto; import apptive.devlog.domain.user.service.UserService; import apptive.devlog.global.annotation.InjectEmail; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +@Tag(name = "User", description = "유저 관련 API") @RestController @RequestMapping("/user") @RequiredArgsConstructor -public class UserController { +public class UserController implements UserDocumentation { private final UserService userService; + @Operation(summary = "유저 프로필 조회", description = "이메일을 기반으로 유저 프로필 정보를 조회합니다.") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = @Content(schema = @Schema(implementation = ApiResponse.class)) + ) + }) @GetMapping("/profile") - public ResponseEntity> getUserProfile(@Valid @InjectEmail UserProfileRequestDto requestDto) { + public ResponseEntity> getUserProfile(@Parameter(description = "조회할 유저의 이메일", example = "test@example.com") @Valid @InjectEmail UserProfileRequestDto requestDto) { UserProfileResponseDto responseDto = userService.getUserProfile(requestDto); - return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.ok(responseDto)); + return ResponseEntity.status(HttpStatus.OK).body(CommonResponse.ok(responseDto)); } } diff --git a/src/main/java/apptive/devlog/domain/user/dto/UserProfileRequestDto.java b/src/main/java/apptive/devlog/domain/user/dto/UserProfileRequestDto.java index 111c5ee..5169e76 100644 --- a/src/main/java/apptive/devlog/domain/user/dto/UserProfileRequestDto.java +++ b/src/main/java/apptive/devlog/domain/user/dto/UserProfileRequestDto.java @@ -1,12 +1,15 @@ package apptive.devlog.domain.user.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor +@Schema(description = "유저 프로필 요청 DTO") public class UserProfileRequestDto { + @Schema(description = "유저 이메일", example = "user@example.com", required = true) @NotBlank private String email; } diff --git a/src/main/java/apptive/devlog/domain/user/dto/UserProfileResponseDto.java b/src/main/java/apptive/devlog/domain/user/dto/UserProfileResponseDto.java index 196040f..825adf2 100644 --- a/src/main/java/apptive/devlog/domain/user/dto/UserProfileResponseDto.java +++ b/src/main/java/apptive/devlog/domain/user/dto/UserProfileResponseDto.java @@ -2,16 +2,23 @@ import apptive.devlog.domain.user.entity.User; import apptive.devlog.domain.user.enums.Gender; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import java.time.LocalDate; @Getter +@Schema(description = "유저 프로필 응답 DTO") public class UserProfileResponseDto { + @Schema(description = "이메일", example = "user@example.com") private final String email; + @Schema(description = "닉네임", example = "nickname123") private final String nickname; + @Schema(description = "이름", example = "홍길동") private final String name; + @Schema(description = "생일", example = "1995-05-20") private final LocalDate birthday; + @Schema(description = "성별", example = "MALE") private final Gender gender; public UserProfileResponseDto(User user) { diff --git a/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java b/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java index 0d94ef7..f7d6c28 100644 --- a/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java +++ b/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java @@ -6,7 +6,6 @@ import apptive.devlog.domain.oauth2.service.CustomOAuth2UserService; import apptive.devlog.global.security.service.CustomUserDetailsService; import apptive.devlog.global.security.jwt.JwtTokenProvider; -import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -58,6 +57,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/auth/signup", "/auth/login", "/auth/refresh", "/oauth2/**").permitAll() + .requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/api-docs/**").permitAll() .requestMatchers("/auth/logout", "/user/profile").hasRole("USER") .anyRequest().authenticated() ) diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index 2bcfadb..3059a9e 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -34,6 +34,14 @@ spring.data.redis.port=6379 logging.level.org.springframework.security=DEBUG logging.level.com.apptive.devlog=DEBUG +# =============================== +# SWAGGER +# =============================== +springdoc.api-docs.path=/api-docs +springdoc.swagger-ui.path=/swagger-ui.html +springdoc.default-consumes-media-type=application/json +springdoc.default-produces-media-type=application/json + # =============================== # JWT CONFIG # =============================== From 655d13d358e617a94dafbc6bef0e2a8806f92c76 Mon Sep 17 00:00:00 2001 From: qqwwee771441 <104188251+qqwwee771441@users.noreply.github.com> Date: Wed, 9 Apr 2025 07:28:22 +0900 Subject: [PATCH 06/11] fix: SwaggerGroupConfig/java --- .../devlog/documentation/config/SwaggerGroupConfig.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/apptive/devlog/documentation/config/SwaggerGroupConfig.java b/src/main/java/apptive/devlog/documentation/config/SwaggerGroupConfig.java index 638f15e..ab6a869 100644 --- a/src/main/java/apptive/devlog/documentation/config/SwaggerGroupConfig.java +++ b/src/main/java/apptive/devlog/documentation/config/SwaggerGroupConfig.java @@ -10,16 +10,16 @@ public class SwaggerGroupConfig { @Bean public GroupedOpenApi publicApiV1() { return GroupedOpenApi.builder() - .group("v1") - .pathsToMatch("/api/v1/**") + .group("auth") + .pathsToMatch("/auth/**") .build(); } @Bean public GroupedOpenApi publicApiV2() { return GroupedOpenApi.builder() - .group("v2") - .pathsToMatch("/api/v2/**") + .group("user") + .pathsToMatch("/user/**") .build(); } } From b2abad56fcb4bc6894aaccb8a64786688261b3aa Mon Sep 17 00:00:00 2001 From: qqwwee771441 <104188251+qqwwee771441@users.noreply.github.com> Date: Thu, 10 Apr 2025 09:53:38 +0900 Subject: [PATCH 07/11] feat: hw2 --- build.gradle | 2 + .../devlog/common/base/BaseEntity.java | 16 +++- .../documentation/auth/AuthLoginDoc.java | 20 +++++ .../documentation/auth/AuthLogoutDoc.java | 15 ++++ .../documentation/auth/AuthRefreshDoc.java | 22 +++++ .../documentation/auth/AuthSignupDoc.java | 22 +++++ .../comment/CommentCreateDoc.java | 9 ++ .../comment/CommentDeleteDoc.java | 9 ++ .../documentation/comment/CommentReadDoc.java | 7 ++ .../comment/CommentUpdateDoc.java | 9 ++ .../common/GlobalApiResponse.java | 29 +++++++ .../documentation/config/SwaggerConfig.java | 11 +++ .../config/SwaggerGroupConfig.java | 20 ++++- .../documentation/post/PostCreateDoc.java | 9 ++ .../documentation/post/PostDeleteDoc.java | 9 ++ .../documentation/post/PostReadDoc.java | 7 ++ .../documentation/post/PostUpdateDoc.java | 9 ++ .../tags/CommentDocumentation.java | 7 ++ .../documentation/tags/PostDocumentation.java | 7 ++ .../documentation/user/UserProfileDoc.java | 26 ++++++ .../auth/controller/AuthController.java | 48 ++--------- .../comment/controller/CommentController.java | 52 ++++++++++++ .../devlog/domain/comment/dto/CommentDto.java | 17 ++++ .../devlog/domain/comment/entity/Comment.java | 48 +++++++++++ .../comment/repository/CommentRepository.java | 20 +++++ .../comment/service/CommentService.java | 83 +++++++++++++++++++ .../post/controller/PostController.java | 50 +++++++++++ .../devlog/domain/post/dto/PostDto.java | 16 ++++ .../devlog/domain/post/entity/Post.java | 39 +++++++++ .../post/repository/PostRepository.java | 9 ++ .../domain/post/service/PostService.java | 47 +++++++++++ .../user/controller/UserController.java | 20 +---- .../devlog/domain/user/entity/User.java | 5 +- .../user/repository/UserRepository.java | 3 +- .../domain/user/service/UserService.java | 1 + .../devlog/global/config/WebConfig.java | 8 ++ .../security/config/SecurityConfig.java | 3 +- .../filter/JwtAuthenticationFilter.java | 10 +-- .../JwtAuthenticationInterceptor.java | 28 +++++++ .../global/security/jwt/JwtTokenProvider.java | 9 ++ .../resources/application-local.properties | 5 +- .../controller/CommentControllerTest.java | 4 + .../comment/service/CommentServiceTest.java | 4 + .../post/controller/PostControllerTest.java | 4 + .../domain/post/service/PostServiceTest.java | 4 + 45 files changed, 722 insertions(+), 80 deletions(-) create mode 100644 src/main/java/apptive/devlog/documentation/auth/AuthLoginDoc.java create mode 100644 src/main/java/apptive/devlog/documentation/auth/AuthLogoutDoc.java create mode 100644 src/main/java/apptive/devlog/documentation/auth/AuthRefreshDoc.java create mode 100644 src/main/java/apptive/devlog/documentation/auth/AuthSignupDoc.java create mode 100644 src/main/java/apptive/devlog/documentation/comment/CommentCreateDoc.java create mode 100644 src/main/java/apptive/devlog/documentation/comment/CommentDeleteDoc.java create mode 100644 src/main/java/apptive/devlog/documentation/comment/CommentReadDoc.java create mode 100644 src/main/java/apptive/devlog/documentation/comment/CommentUpdateDoc.java create mode 100644 src/main/java/apptive/devlog/documentation/common/GlobalApiResponse.java create mode 100644 src/main/java/apptive/devlog/documentation/post/PostCreateDoc.java create mode 100644 src/main/java/apptive/devlog/documentation/post/PostDeleteDoc.java create mode 100644 src/main/java/apptive/devlog/documentation/post/PostReadDoc.java create mode 100644 src/main/java/apptive/devlog/documentation/post/PostUpdateDoc.java create mode 100644 src/main/java/apptive/devlog/documentation/tags/CommentDocumentation.java create mode 100644 src/main/java/apptive/devlog/documentation/tags/PostDocumentation.java create mode 100644 src/main/java/apptive/devlog/documentation/user/UserProfileDoc.java create mode 100644 src/main/java/apptive/devlog/domain/comment/controller/CommentController.java create mode 100644 src/main/java/apptive/devlog/domain/comment/dto/CommentDto.java create mode 100644 src/main/java/apptive/devlog/domain/comment/entity/Comment.java create mode 100644 src/main/java/apptive/devlog/domain/comment/repository/CommentRepository.java create mode 100644 src/main/java/apptive/devlog/domain/comment/service/CommentService.java create mode 100644 src/main/java/apptive/devlog/domain/post/controller/PostController.java create mode 100644 src/main/java/apptive/devlog/domain/post/dto/PostDto.java create mode 100644 src/main/java/apptive/devlog/domain/post/entity/Post.java create mode 100644 src/main/java/apptive/devlog/domain/post/repository/PostRepository.java create mode 100644 src/main/java/apptive/devlog/domain/post/service/PostService.java create mode 100644 src/main/java/apptive/devlog/global/security/interceptor/JwtAuthenticationInterceptor.java create mode 100644 src/test/java/apptive/devlog/domain/comment/controller/CommentControllerTest.java create mode 100644 src/test/java/apptive/devlog/domain/comment/service/CommentServiceTest.java create mode 100644 src/test/java/apptive/devlog/domain/post/controller/PostControllerTest.java create mode 100644 src/test/java/apptive/devlog/domain/post/service/PostServiceTest.java diff --git a/build.gradle b/build.gradle index 059d872..e8fecc6 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + implementation 'de.huxhorn.sulky:de.huxhorn.sulky.ulid:8.2.0' + implementation 'com.github.f4b6a3:ulid-creator:5.2.0' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' diff --git a/src/main/java/apptive/devlog/common/base/BaseEntity.java b/src/main/java/apptive/devlog/common/base/BaseEntity.java index 6536fd2..f30ab3e 100644 --- a/src/main/java/apptive/devlog/common/base/BaseEntity.java +++ b/src/main/java/apptive/devlog/common/base/BaseEntity.java @@ -1,8 +1,6 @@ package apptive.devlog.common.base; -import jakarta.persistence.Column; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.*; import lombok.Getter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; @@ -14,6 +12,11 @@ @EntityListeners(AuditingEntityListener.class) @Getter public abstract class BaseEntity { + + @Id + @Column(length = 26, updatable = false, nullable = false) + private String id; + @CreatedDate @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @@ -21,4 +24,11 @@ public abstract class BaseEntity { @LastModifiedDate @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; + + @PrePersist + public void assignId() { + if (this.id == null) { + this.id = com.github.f4b6a3.ulid.UlidCreator.getUlid().toString(); + } + } } diff --git a/src/main/java/apptive/devlog/documentation/auth/AuthLoginDoc.java b/src/main/java/apptive/devlog/documentation/auth/AuthLoginDoc.java new file mode 100644 index 0000000..b533c83 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/auth/AuthLoginDoc.java @@ -0,0 +1,20 @@ +package apptive.devlog.documentation.auth; + +import apptive.devlog.documentation.wrapper.UserLoginResponseWrapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +@Operation(summary = "회원 로그인", description = "이메일과 비밀번호로 로그인합니다.") +@ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UserLoginResponseWrapper.class) + )), + @ApiResponse(responseCode = "401", description = "인증 실패", content = @Content) +}) +public @interface AuthLoginDoc { +} diff --git a/src/main/java/apptive/devlog/documentation/auth/AuthLogoutDoc.java b/src/main/java/apptive/devlog/documentation/auth/AuthLogoutDoc.java new file mode 100644 index 0000000..6388b8a --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/auth/AuthLogoutDoc.java @@ -0,0 +1,15 @@ +package apptive.devlog.documentation.auth; + +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.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +@Operation(summary = "로그아웃", description = "로그아웃 처리합니다.") +@ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "로그아웃 성공", content = @Content) +}) +@SecurityRequirement(name = "bearerAuth") +public @interface AuthLogoutDoc { +} diff --git a/src/main/java/apptive/devlog/documentation/auth/AuthRefreshDoc.java b/src/main/java/apptive/devlog/documentation/auth/AuthRefreshDoc.java new file mode 100644 index 0000000..595ed85 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/auth/AuthRefreshDoc.java @@ -0,0 +1,22 @@ +package apptive.devlog.documentation.auth; + +import apptive.devlog.documentation.wrapper.UserRefreshResponseWrapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +@Operation(summary = "토큰 재발급", description = "Refresh Token을 이용하여 Access Token을 재발급합니다.") +@ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "토큰 재발급 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UserRefreshResponseWrapper.class) + )), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰", content = @Content) +}) +@SecurityRequirement(name = "bearerAuth") +public @interface AuthRefreshDoc { +} diff --git a/src/main/java/apptive/devlog/documentation/auth/AuthSignupDoc.java b/src/main/java/apptive/devlog/documentation/auth/AuthSignupDoc.java new file mode 100644 index 0000000..d7e2f3a --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/auth/AuthSignupDoc.java @@ -0,0 +1,22 @@ +package apptive.devlog.documentation.auth; + +import apptive.devlog.common.response.success.CommonResponse; +import apptive.devlog.documentation.wrapper.UserSignupResponseWrapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +@Operation(summary = "회원 가입", description = "회원으로 가입합니다.") +@ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "회원가입 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UserSignupResponseWrapper.class) + )), + @ApiResponse(responseCode = "400", description = "요청 파라미터 오류 또는 중복된 이메일", + content = @Content(schema = @Schema(implementation = CommonResponse.class))) +}) +public @interface AuthSignupDoc { +} diff --git a/src/main/java/apptive/devlog/documentation/comment/CommentCreateDoc.java b/src/main/java/apptive/devlog/documentation/comment/CommentCreateDoc.java new file mode 100644 index 0000000..947ee8d --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/comment/CommentCreateDoc.java @@ -0,0 +1,9 @@ +package apptive.devlog.documentation.comment; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +@Operation(summary = "댓글 작성", description = "게시글에 댓글을 작성합니다.") +@SecurityRequirement(name = "bearerAuth") +public @interface CommentCreateDoc { +} diff --git a/src/main/java/apptive/devlog/documentation/comment/CommentDeleteDoc.java b/src/main/java/apptive/devlog/documentation/comment/CommentDeleteDoc.java new file mode 100644 index 0000000..9d8310a --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/comment/CommentDeleteDoc.java @@ -0,0 +1,9 @@ +package apptive.devlog.documentation.comment; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +@Operation(summary = "댓글 삭제", description = "본인의 댓글을 삭제합니다.") +@SecurityRequirement(name = "bearerAuth") +public @interface CommentDeleteDoc { +} diff --git a/src/main/java/apptive/devlog/documentation/comment/CommentReadDoc.java b/src/main/java/apptive/devlog/documentation/comment/CommentReadDoc.java new file mode 100644 index 0000000..3799153 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/comment/CommentReadDoc.java @@ -0,0 +1,7 @@ +package apptive.devlog.documentation.comment; + +import io.swagger.v3.oas.annotations.Operation; + +@Operation(summary = "댓글 목록 조회", description = "게시글의 댓글 목록을 조회합니다.") +public @interface CommentReadDoc { +} diff --git a/src/main/java/apptive/devlog/documentation/comment/CommentUpdateDoc.java b/src/main/java/apptive/devlog/documentation/comment/CommentUpdateDoc.java new file mode 100644 index 0000000..c51d8c9 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/comment/CommentUpdateDoc.java @@ -0,0 +1,9 @@ +package apptive.devlog.documentation.comment; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +@Operation(summary = "댓글 수정", description = "본인의 댓글을 수정합니다.") +@SecurityRequirement(name = "bearerAuth") +public @interface CommentUpdateDoc { +} diff --git a/src/main/java/apptive/devlog/documentation/common/GlobalApiResponse.java b/src/main/java/apptive/devlog/documentation/common/GlobalApiResponse.java new file mode 100644 index 0000000..78c5192 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/common/GlobalApiResponse.java @@ -0,0 +1,29 @@ +package apptive.devlog.documentation.common; + +import apptive.devlog.common.response.success.CommonResponse; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +import java.lang.annotation.*; + +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ApiResponses({ + @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "201", description = "리소스 생성 성공", content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "204", description = "응답 본문 없음", content = @Content(schema = @Schema())), + @ApiResponse(responseCode = "400", description = "잘못된 요청 (유효성 검증 실패 등)", content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "401", description = "인증되지 않음 (토큰 없음 또는 만료)", content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "403", description = "권한 없음", content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "404", description = "리소스를 찾을 수 없음", content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "409", description = "충돌 (중복 데이터 등)", content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "422", description = "처리할 수 없는 요청 (비즈니스 로직 실패 등)", content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "429", description = "너무 많은 요청", content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "500", description = "서버 내부 오류", content = @Content(schema = @Schema(implementation = CommonResponse.class))), + @ApiResponse(responseCode = "503", description = "서비스 이용 불가", content = @Content(schema = @Schema(implementation = CommonResponse.class))) +}) +public @interface GlobalApiResponse { +} diff --git a/src/main/java/apptive/devlog/documentation/config/SwaggerConfig.java b/src/main/java/apptive/devlog/documentation/config/SwaggerConfig.java index 4e8a9ae..0e6643e 100644 --- a/src/main/java/apptive/devlog/documentation/config/SwaggerConfig.java +++ b/src/main/java/apptive/devlog/documentation/config/SwaggerConfig.java @@ -1,5 +1,7 @@ package apptive.devlog.documentation.config; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; @@ -9,6 +11,15 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +@OpenAPIDefinition( + info = @io.swagger.v3.oas.annotations.info.Info(title = "Devlog API", version = "v1") +) +@io.swagger.v3.oas.annotations.security.SecurityScheme( + name = "bearerAuth", + type = SecuritySchemeType.HTTP, + scheme = "bearer", + bearerFormat = "JWT" +) @Configuration public class SwaggerConfig { diff --git a/src/main/java/apptive/devlog/documentation/config/SwaggerGroupConfig.java b/src/main/java/apptive/devlog/documentation/config/SwaggerGroupConfig.java index ab6a869..b0acda2 100644 --- a/src/main/java/apptive/devlog/documentation/config/SwaggerGroupConfig.java +++ b/src/main/java/apptive/devlog/documentation/config/SwaggerGroupConfig.java @@ -8,7 +8,15 @@ public class SwaggerGroupConfig { @Bean - public GroupedOpenApi publicApiV1() { + public GroupedOpenApi defaultAllApi() { + return GroupedOpenApi.builder() + .group("all") + .pathsToMatch("/**") + .build(); + } + + @Bean + public GroupedOpenApi publicApiAuth() { return GroupedOpenApi.builder() .group("auth") .pathsToMatch("/auth/**") @@ -16,10 +24,18 @@ public GroupedOpenApi publicApiV1() { } @Bean - public GroupedOpenApi publicApiV2() { + public GroupedOpenApi publicApiUser() { return GroupedOpenApi.builder() .group("user") .pathsToMatch("/user/**") .build(); } + + @Bean + public GroupedOpenApi publicApiPost() { + return GroupedOpenApi.builder() + .group("post") + .pathsToMatch("/post/**") + .build(); + } } diff --git a/src/main/java/apptive/devlog/documentation/post/PostCreateDoc.java b/src/main/java/apptive/devlog/documentation/post/PostCreateDoc.java new file mode 100644 index 0000000..48ad831 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/post/PostCreateDoc.java @@ -0,0 +1,9 @@ +package apptive.devlog.documentation.post; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +@Operation(summary = "게시글 작성") +@SecurityRequirement(name = "bearerAuth") +public @interface PostCreateDoc { +} diff --git a/src/main/java/apptive/devlog/documentation/post/PostDeleteDoc.java b/src/main/java/apptive/devlog/documentation/post/PostDeleteDoc.java new file mode 100644 index 0000000..6ec5f07 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/post/PostDeleteDoc.java @@ -0,0 +1,9 @@ +package apptive.devlog.documentation.post; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +@Operation(summary = "게시글 삭제") +@SecurityRequirement(name = "bearerAuth") +public @interface PostDeleteDoc { +} diff --git a/src/main/java/apptive/devlog/documentation/post/PostReadDoc.java b/src/main/java/apptive/devlog/documentation/post/PostReadDoc.java new file mode 100644 index 0000000..2deae5a --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/post/PostReadDoc.java @@ -0,0 +1,7 @@ +package apptive.devlog.documentation.post; + +import io.swagger.v3.oas.annotations.Operation; + +@Operation(summary = "게시글 조회") +public @interface PostReadDoc { +} diff --git a/src/main/java/apptive/devlog/documentation/post/PostUpdateDoc.java b/src/main/java/apptive/devlog/documentation/post/PostUpdateDoc.java new file mode 100644 index 0000000..0d0436b --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/post/PostUpdateDoc.java @@ -0,0 +1,9 @@ +package apptive.devlog.documentation.post; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +@Operation(summary = "게시글 수정") +@SecurityRequirement(name = "bearerAuth") +public @interface PostUpdateDoc { +} diff --git a/src/main/java/apptive/devlog/documentation/tags/CommentDocumentation.java b/src/main/java/apptive/devlog/documentation/tags/CommentDocumentation.java new file mode 100644 index 0000000..362df96 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/tags/CommentDocumentation.java @@ -0,0 +1,7 @@ +package apptive.devlog.documentation.tags; + +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Comment", description = "Comment 관련 API (생성, 조회, 수정, 삭제)") +public interface CommentDocumentation { +} diff --git a/src/main/java/apptive/devlog/documentation/tags/PostDocumentation.java b/src/main/java/apptive/devlog/documentation/tags/PostDocumentation.java new file mode 100644 index 0000000..daee766 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/tags/PostDocumentation.java @@ -0,0 +1,7 @@ +package apptive.devlog.documentation.tags; + +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Post", description = "게시글 관련 API (생성, 조회, 수정, 삭제)") +public interface PostDocumentation { +} diff --git a/src/main/java/apptive/devlog/documentation/user/UserProfileDoc.java b/src/main/java/apptive/devlog/documentation/user/UserProfileDoc.java new file mode 100644 index 0000000..fe6dd58 --- /dev/null +++ b/src/main/java/apptive/devlog/documentation/user/UserProfileDoc.java @@ -0,0 +1,26 @@ +package apptive.devlog.documentation.user; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +@Operation(summary = "유저 프로필 조회", description = "이메일을 기반으로 유저 프로필 정보를 조회합니다.") +@ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = @Content(schema = @Schema(implementation = ApiResponse.class)) + ) +}) +@Parameter( + name = "requestDto", + description = "조회할 유저의 이메일", + example = "test@example.com" +) +@SecurityRequirement(name = "bearerAuth") +public @interface UserProfileDoc { +} diff --git a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java index e258abd..2dee54d 100644 --- a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java +++ b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java @@ -1,18 +1,14 @@ package apptive.devlog.domain.auth.controller; import apptive.devlog.common.response.success.CommonResponse; +import apptive.devlog.documentation.auth.AuthLoginDoc; +import apptive.devlog.documentation.auth.AuthLogoutDoc; +import apptive.devlog.documentation.auth.AuthRefreshDoc; +import apptive.devlog.documentation.auth.AuthSignupDoc; import apptive.devlog.documentation.tags.AuthDocumentation; -import apptive.devlog.documentation.wrapper.UserLoginResponseWrapper; -import apptive.devlog.documentation.wrapper.UserRefreshResponseWrapper; -import apptive.devlog.documentation.wrapper.UserSignupResponseWrapper; import apptive.devlog.domain.auth.dto.*; import apptive.devlog.domain.auth.service.AuthService; import apptive.devlog.global.annotation.InjectToken; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -26,56 +22,28 @@ public class AuthController implements AuthDocumentation { private final AuthService authService; - @Operation(summary = "회원 가입", description = "회원으로 가입합니다.") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "회원가입 성공", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = UserSignupResponseWrapper.class) - )), - @ApiResponse(responseCode = "400", description = "요청 파라미터 오류 또는 중복된 이메일", - content = @Content(schema = @Schema(implementation = CommonResponse.class))) - }) + @AuthSignupDoc @PostMapping("/signup") public ResponseEntity> signup(@Valid @RequestBody UserSignupRequestDto requestDto) { UserSignupResponseDto responseDto = authService.signup(requestDto); return ResponseEntity.status(HttpStatus.CREATED).body(CommonResponse.created(responseDto)); } - @Operation(summary = "회원 로그인", description = "이메일과 비밀번호로 로그인합니다.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "로그인 성공", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = UserLoginResponseWrapper.class) - )), - @ApiResponse(responseCode = "401", description = "인증 실패", content = @Content) - }) + @AuthLoginDoc @PostMapping("/login") public ResponseEntity> login(@Valid @RequestBody UserLoginRequestDto requestDto) { UserLoginResponseDto responseDto = authService.login(requestDto); return ResponseEntity.ok(CommonResponse.ok(responseDto)); } - @Operation(summary = "토큰 재발급", description = "Refresh Token을 이용하여 Access Token을 재발급합니다.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "토큰 재발급 성공", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = UserRefreshResponseWrapper.class) - )), - @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰", content = @Content) - }) + @AuthRefreshDoc @PostMapping("/refresh") public ResponseEntity> refresh(@Valid @InjectToken UserRefreshRequestDto requestDto) { UserRefreshResponseDto responseDto = authService.refresh(requestDto); return ResponseEntity.ok(CommonResponse.ok(responseDto)); } - @Operation(summary = "로그아웃", description = "로그아웃 처리합니다.") - @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "로그아웃 성공", content = @Content) - }) + @AuthLogoutDoc @PostMapping("/logout") public ResponseEntity> logout(@Valid @InjectToken UserLogoutRequestDto requestDto) { authService.logout(requestDto); diff --git a/src/main/java/apptive/devlog/domain/comment/controller/CommentController.java b/src/main/java/apptive/devlog/domain/comment/controller/CommentController.java new file mode 100644 index 0000000..81660cb --- /dev/null +++ b/src/main/java/apptive/devlog/domain/comment/controller/CommentController.java @@ -0,0 +1,52 @@ +package apptive.devlog.domain.comment.controller; + +import apptive.devlog.common.response.success.CommonResponse; +import apptive.devlog.documentation.comment.CommentCreateDoc; +import apptive.devlog.documentation.comment.CommentDeleteDoc; +import apptive.devlog.documentation.comment.CommentReadDoc; +import apptive.devlog.documentation.comment.CommentUpdateDoc; +import apptive.devlog.documentation.tags.CommentDocumentation; +import apptive.devlog.domain.comment.dto.CommentDto; +import apptive.devlog.domain.comment.service.CommentService; +import apptive.devlog.domain.user.entity.User; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("post/{postId}/comment") +@RequiredArgsConstructor +public class CommentController implements CommentDocumentation { + private final CommentService commentService; + + @CommentCreateDoc + @PostMapping + public ResponseEntity> create(@PathVariable String postId, @Valid @RequestBody CommentDto.Create dto, @RequestAttribute("user") User user) { + CommentDto.Response response = commentService.create(postId, dto, user); + return ResponseEntity.status(HttpStatus.CREATED).body(CommonResponse.created(response)); + } + + @CommentReadDoc + @GetMapping + public ResponseEntity>> read(@PathVariable String postId) { + return ResponseEntity.ok().body(CommonResponse.ok(commentService.read(postId))); + } + + @CommentUpdateDoc + @PutMapping("/{commentId}") + public ResponseEntity> update(@PathVariable String postId, @PathVariable String commentId, @Valid @RequestBody CommentDto.Update dto, @RequestAttribute("user") User user) { + CommentDto.Response response = commentService.update(commentId, dto, user); + return ResponseEntity.ok().body(CommonResponse.ok(response)); + } + + @CommentDeleteDoc + @DeleteMapping("/{commentId}") + public ResponseEntity> delete(@PathVariable String postId, @PathVariable String commentId, @RequestAttribute("user") User user) { + commentService.delete(commentId, user); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/apptive/devlog/domain/comment/dto/CommentDto.java b/src/main/java/apptive/devlog/domain/comment/dto/CommentDto.java new file mode 100644 index 0000000..ddd598f --- /dev/null +++ b/src/main/java/apptive/devlog/domain/comment/dto/CommentDto.java @@ -0,0 +1,17 @@ +package apptive.devlog.domain.comment.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; + +import java.util.List; + +public class CommentDto { + @Builder + public record Create(@NotBlank String text, String parentId) {} + + @Builder + public record Update(@NotBlank String text) {} + + @Builder + public record Response(String id, String text, String authorId, String parentId, List replies) {} +} diff --git a/src/main/java/apptive/devlog/domain/comment/entity/Comment.java b/src/main/java/apptive/devlog/domain/comment/entity/Comment.java new file mode 100644 index 0000000..110409d --- /dev/null +++ b/src/main/java/apptive/devlog/domain/comment/entity/Comment.java @@ -0,0 +1,48 @@ +package apptive.devlog.domain.comment.entity; + +import apptive.devlog.common.base.BaseEntity; +import apptive.devlog.domain.post.entity.Post; +import apptive.devlog.domain.user.entity.User; +import de.huxhorn.sulky.ulid.ULID; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Table(name = "comments") +public class Comment extends BaseEntity { + @NotBlank + @Column(columnDefinition = "TEXT") + private String text; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id", nullable = false) + private User author; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Comment parent; + + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + private List replies; + + @Column(nullable = false) + private boolean deleted; + + @Transient + private int depth; + + public void updateText(String newText) { + this.text = newText; + } +} diff --git a/src/main/java/apptive/devlog/domain/comment/repository/CommentRepository.java b/src/main/java/apptive/devlog/domain/comment/repository/CommentRepository.java new file mode 100644 index 0000000..b18302b --- /dev/null +++ b/src/main/java/apptive/devlog/domain/comment/repository/CommentRepository.java @@ -0,0 +1,20 @@ +package apptive.devlog.domain.comment.repository; + +import apptive.devlog.domain.comment.entity.Comment; +import apptive.devlog.domain.post.entity.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface CommentRepository extends JpaRepository { + List findByPostIdAndParentIsNullOrderByCreatedAtDesc(String postId); + List findByParentIdOrderByCreatedAtDesc(String parentId); + + List findByPostAndParentIsNull(Post post); + Optional findByIdAndDeletedFalse(String id); + + String post(Post post); +} diff --git a/src/main/java/apptive/devlog/domain/comment/service/CommentService.java b/src/main/java/apptive/devlog/domain/comment/service/CommentService.java new file mode 100644 index 0000000..c40a411 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/comment/service/CommentService.java @@ -0,0 +1,83 @@ +package apptive.devlog.domain.comment.service; + +import apptive.devlog.domain.comment.dto.CommentDto; +import apptive.devlog.domain.comment.entity.Comment; +import apptive.devlog.domain.comment.repository.CommentRepository; +import apptive.devlog.domain.post.entity.Post; +import apptive.devlog.domain.post.repository.PostRepository; +import apptive.devlog.domain.user.entity.User; +import apptive.devlog.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.apache.coyote.BadRequestException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CommentService { + private final CommentRepository commentRepository; + private final PostRepository postRepository; + private final UserRepository userRepository; + + private static final int MAX_DEPTH = 3; + + @Transactional + public CommentDto.Response create(String postId, CommentDto.Create dto, User author) { + Post post = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("post not found")); + User user = userRepository.findById(author.getId()).orElseThrow(() -> new IllegalArgumentException("user not found")); + + Comment parent = null; + int depth = 0; + if (dto.parentId() != null) { + parent = commentRepository.findByIdAndDeletedFalse(dto.parentId()).orElseThrow(() -> new IllegalArgumentException("parent comment not found")); + depth = calculateDepth(parent); + if (depth >= MAX_DEPTH) { + throw new IllegalArgumentException("댓글은 최대 %d단계까지만 가능합니다.".formatted(MAX_DEPTH)); + } + } + + Comment comment = Comment.builder().text(dto.text()).author(author).parent(parent).build(); + commentRepository.save(comment); + return toResponse(comment); + } + + @Transactional(readOnly = true) + public List read(String postId) { + return commentRepository.findByPostIdAndParentIsNullOrderByCreatedAtDesc(postId).stream().map(this::toResponse).toList(); + } + + @Transactional + public CommentDto.Response update(String commentId, CommentDto.Update dto, User user) { + Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new IllegalArgumentException("comment not found")); + if (!comment.getAuthor().getId().equals(user.getId())) { + throw new IllegalArgumentException("수정 권한이 없습니다"); + } + comment.updateText(dto.text()); + return toResponse(comment); + } + + @Transactional + public void delete(String commentId, User user) { + Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new IllegalArgumentException("comment not found")); + if (!comment.getAuthor().getId().equals(user.getId())) { + throw new IllegalArgumentException("삭제 권한이 없습니다."); + } + commentRepository.delete(comment); + } + + private int calculateDepth(Comment comment) { + int depth = 0; + while (comment.getParent() != null) { + comment = comment.getParent(); + depth++; + } + return depth; + } + + private CommentDto.Response toResponse(Comment comment) { + List replies = comment.getReplies() == null ? List.of() : comment.getReplies().stream().map(this::toResponse).toList(); + return CommentDto.Response.builder().id(comment.getId()).text(comment.getText()).authorId(comment.getAuthor().getId()).parentId(comment.getParent() != null ? comment.getParent().getId() : null).replies(replies).build(); + } +} diff --git a/src/main/java/apptive/devlog/domain/post/controller/PostController.java b/src/main/java/apptive/devlog/domain/post/controller/PostController.java new file mode 100644 index 0000000..40177ee --- /dev/null +++ b/src/main/java/apptive/devlog/domain/post/controller/PostController.java @@ -0,0 +1,50 @@ +package apptive.devlog.domain.post.controller; + +import apptive.devlog.common.response.success.CommonResponse; +import apptive.devlog.documentation.post.PostCreateDoc; +import apptive.devlog.documentation.post.PostDeleteDoc; +import apptive.devlog.documentation.post.PostReadDoc; +import apptive.devlog.documentation.post.PostUpdateDoc; +import apptive.devlog.documentation.tags.PostDocumentation; +import apptive.devlog.domain.post.dto.PostDto; +import apptive.devlog.domain.post.service.PostService; +import apptive.devlog.domain.user.entity.User; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/post") +@RequiredArgsConstructor +public class PostController implements PostDocumentation { + private final PostService postService; + + @PostCreateDoc + @PostMapping + public ResponseEntity> create(@Valid @RequestBody PostDto.Create postDto, @RequestAttribute("user") User user) { + PostDto.Response responseDto = postService.create(postDto, user); + return ResponseEntity.status(HttpStatus.CREATED).body(CommonResponse.created(responseDto)); + } + + @PostReadDoc + @GetMapping("/{id}") + public ResponseEntity> read(@PathVariable String id) { + return ResponseEntity.ok().body(CommonResponse.ok(postService.read(id))); + } + + @PostUpdateDoc + @PutMapping("/{id}") + public ResponseEntity> update(@PathVariable String id, @Valid @RequestBody PostDto.Update dto, @RequestAttribute("user") User user) { + PostDto.Response response = postService.update(id, dto, user); + return ResponseEntity.ok().body(CommonResponse.ok(response)); + } + + @PostDeleteDoc + @DeleteMapping("/{id}") + public ResponseEntity> delete(@PathVariable String id, @RequestAttribute("user") User user) { + postService.delete(id, user); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/apptive/devlog/domain/post/dto/PostDto.java b/src/main/java/apptive/devlog/domain/post/dto/PostDto.java new file mode 100644 index 0000000..3ce9a77 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/post/dto/PostDto.java @@ -0,0 +1,16 @@ +package apptive.devlog.domain.post.dto; + +import apptive.devlog.domain.user.entity.User; +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; + +public class PostDto { + @Builder + public record Create(@NotBlank String title, @NotBlank String content) {} + + @Builder + public record Update(@NotBlank String title, @NotBlank String content) {} + + @Builder + public record Response(String id, String title, String content, String authorId) {} +} diff --git a/src/main/java/apptive/devlog/domain/post/entity/Post.java b/src/main/java/apptive/devlog/domain/post/entity/Post.java new file mode 100644 index 0000000..1ac0809 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/post/entity/Post.java @@ -0,0 +1,39 @@ +package apptive.devlog.domain.post.entity; + +import apptive.devlog.common.base.BaseEntity; +import apptive.devlog.domain.comment.entity.Comment; +import apptive.devlog.domain.user.entity.User; +import de.huxhorn.sulky.ulid.ULID; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Table(name = "posts") +public class Post extends BaseEntity { + @NotBlank + private String title; + + @NotBlank + @Column(columnDefinition = "TEXT") + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id", nullable = false) + private User author; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public void update(String title, String content) { + this.title = title; + this.content = content; + } +} diff --git a/src/main/java/apptive/devlog/domain/post/repository/PostRepository.java b/src/main/java/apptive/devlog/domain/post/repository/PostRepository.java new file mode 100644 index 0000000..beb77a6 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/post/repository/PostRepository.java @@ -0,0 +1,9 @@ +package apptive.devlog.domain.post.repository; + +import apptive.devlog.domain.post.entity.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PostRepository extends JpaRepository { +} diff --git a/src/main/java/apptive/devlog/domain/post/service/PostService.java b/src/main/java/apptive/devlog/domain/post/service/PostService.java new file mode 100644 index 0000000..c4ef2a8 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/post/service/PostService.java @@ -0,0 +1,47 @@ +package apptive.devlog.domain.post.service; + +import apptive.devlog.domain.post.dto.PostDto; +import apptive.devlog.domain.post.entity.Post; +import apptive.devlog.domain.post.repository.PostRepository; +import apptive.devlog.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class PostService { + private final PostRepository postRepository; + + public PostDto.Response create(PostDto.Create dto, User author) { + Post post = Post.builder().title(dto.title()).content(dto.content()).author(author).build(); + postRepository.save(post); + return toResponse(post); + } + + public PostDto.Response read(String postId) { + return postRepository.findById(postId).map(this::toResponse).orElseThrow(() -> new IllegalArgumentException("Post not found")); + } + + public PostDto.Response update(String postId, PostDto.Update dto, User user) { + Post post = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("Post not found")); + if (!post.getAuthor().getId().equals(user.getId())) { + throw new IllegalArgumentException("작성자만 수정할 수 있습니다."); + } + post.update(dto.title(), dto.content()); + return toResponse(post); + } + + public void delete(String postId, User user) { + Post post = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("Post not found")); + if (!post.getAuthor().getId().equals(user.getId())) { + throw new IllegalArgumentException("작성자만 삭제할 수 있습니다."); + } + postRepository.delete(post); + } + + private PostDto.Response toResponse(Post post) { + return PostDto.Response.builder().id(post.getId()).title(post.getTitle()).content(post.getContent()).authorId(post.getAuthor().getId()).build(); + } +} diff --git a/src/main/java/apptive/devlog/domain/user/controller/UserController.java b/src/main/java/apptive/devlog/domain/user/controller/UserController.java index 8e4cf16..aa8020e 100644 --- a/src/main/java/apptive/devlog/domain/user/controller/UserController.java +++ b/src/main/java/apptive/devlog/domain/user/controller/UserController.java @@ -2,24 +2,17 @@ import apptive.devlog.common.response.success.CommonResponse; import apptive.devlog.documentation.tags.UserDocumentation; +import apptive.devlog.documentation.user.UserProfileDoc; import apptive.devlog.domain.user.dto.UserProfileRequestDto; import apptive.devlog.domain.user.dto.UserProfileResponseDto; import apptive.devlog.domain.user.service.UserService; import apptive.devlog.global.annotation.InjectEmail; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -@Tag(name = "User", description = "유저 관련 API") @RestController @RequestMapping("/user") @RequiredArgsConstructor @@ -27,16 +20,9 @@ public class UserController implements UserDocumentation { private final UserService userService; - @Operation(summary = "유저 프로필 조회", description = "이메일을 기반으로 유저 프로필 정보를 조회합니다.") - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "조회 성공", - content = @Content(schema = @Schema(implementation = ApiResponse.class)) - ) - }) + @UserProfileDoc @GetMapping("/profile") - public ResponseEntity> getUserProfile(@Parameter(description = "조회할 유저의 이메일", example = "test@example.com") @Valid @InjectEmail UserProfileRequestDto requestDto) { + public ResponseEntity> getUserProfile(@Valid @InjectEmail UserProfileRequestDto requestDto) { UserProfileResponseDto responseDto = userService.getUserProfile(requestDto); return ResponseEntity.status(HttpStatus.OK).body(CommonResponse.ok(responseDto)); } diff --git a/src/main/java/apptive/devlog/domain/user/entity/User.java b/src/main/java/apptive/devlog/domain/user/entity/User.java index 548c665..d5f69cb 100644 --- a/src/main/java/apptive/devlog/domain/user/entity/User.java +++ b/src/main/java/apptive/devlog/domain/user/entity/User.java @@ -4,6 +4,7 @@ import apptive.devlog.domain.user.enums.Gender; import apptive.devlog.domain.user.enums.Provider; import apptive.devlog.domain.user.enums.Role; +import de.huxhorn.sulky.ulid.ULID; import jakarta.persistence.*; import lombok.*; @@ -19,10 +20,6 @@ @Builder @Table(name = "users") public class User extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @Column(nullable = false, unique = true) private String email; diff --git a/src/main/java/apptive/devlog/domain/user/repository/UserRepository.java b/src/main/java/apptive/devlog/domain/user/repository/UserRepository.java index bddced6..3fe4764 100644 --- a/src/main/java/apptive/devlog/domain/user/repository/UserRepository.java +++ b/src/main/java/apptive/devlog/domain/user/repository/UserRepository.java @@ -5,8 +5,7 @@ import java.util.Optional; -public interface UserRepository extends JpaRepository { +public interface UserRepository extends JpaRepository { Optional findByEmail(String email); boolean existsByEmail(String email); - boolean existsByNickname(String nickname); } diff --git a/src/main/java/apptive/devlog/domain/user/service/UserService.java b/src/main/java/apptive/devlog/domain/user/service/UserService.java index 6a2095c..28cc592 100644 --- a/src/main/java/apptive/devlog/domain/user/service/UserService.java +++ b/src/main/java/apptive/devlog/domain/user/service/UserService.java @@ -7,6 +7,7 @@ import apptive.devlog.common.response.error.exception.UserNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor diff --git a/src/main/java/apptive/devlog/global/config/WebConfig.java b/src/main/java/apptive/devlog/global/config/WebConfig.java index 777c843..2aac3b4 100644 --- a/src/main/java/apptive/devlog/global/config/WebConfig.java +++ b/src/main/java/apptive/devlog/global/config/WebConfig.java @@ -2,11 +2,13 @@ import apptive.devlog.global.resolver.InjectTokenArgumentResolver; import apptive.devlog.global.resolver.InjectEmailArgumentResolver; +import apptive.devlog.global.security.interceptor.JwtAuthenticationInterceptor; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; @@ -16,6 +18,7 @@ public class WebConfig implements WebMvcConfigurer { private final InjectEmailArgumentResolver injectEmailArgumentResolver; private final InjectTokenArgumentResolver injectTokenArgumentResolver; + private final JwtAuthenticationInterceptor jwtAuthenticationInterceptor; @Override public void addArgumentResolvers(List resolvers) { @@ -23,6 +26,11 @@ public void addArgumentResolvers(List resolvers) resolvers.add(injectTokenArgumentResolver); } + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(jwtAuthenticationInterceptor).addPathPatterns("/**"); + } + @Bean public WebClient webClient() { return WebClient.builder().build(); diff --git a/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java b/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java index f7d6c28..116f3c7 100644 --- a/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java +++ b/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java @@ -57,8 +57,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/auth/signup", "/auth/login", "/auth/refresh", "/oauth2/**").permitAll() - .requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/api-docs/**").permitAll() + .requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/v3/api-docs", "/v3/api-docs.yaml", "/swagger-resources/**", "/webjars/**", "/api-docs/**").permitAll() .requestMatchers("/auth/logout", "/user/profile").hasRole("USER") + .requestMatchers("/post/**", "/comment/**").hasRole("USER") .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 diff --git a/src/main/java/apptive/devlog/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/apptive/devlog/global/security/filter/JwtAuthenticationFilter.java index de72bbf..6181f4b 100644 --- a/src/main/java/apptive/devlog/global/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/apptive/devlog/global/security/filter/JwtAuthenticationFilter.java @@ -24,7 +24,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String token = resolveToken(request); + String token = jwtTokenProvider.resolveToken(request); if (token != null && jwtTokenProvider.validateAccessToken(token)) { authenticate(token); } else { @@ -40,12 +40,4 @@ private void authenticate(String token) { SecurityContextHolder.getContext().setAuthentication(authenticationToken); log.debug("Authenticated user: {}", email); } - - private String resolveToken(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (bearerToken != null && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); - } - return null; - } } diff --git a/src/main/java/apptive/devlog/global/security/interceptor/JwtAuthenticationInterceptor.java b/src/main/java/apptive/devlog/global/security/interceptor/JwtAuthenticationInterceptor.java new file mode 100644 index 0000000..f22d4bb --- /dev/null +++ b/src/main/java/apptive/devlog/global/security/interceptor/JwtAuthenticationInterceptor.java @@ -0,0 +1,28 @@ +package apptive.devlog.global.security.interceptor; + +import apptive.devlog.domain.user.entity.User; +import apptive.devlog.domain.user.repository.UserRepository; +import apptive.devlog.global.security.jwt.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationInterceptor implements HandlerInterceptor { + private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String token = jwtTokenProvider.resolveToken(request); + if (token != null && jwtTokenProvider.validateAccessToken(token)) { + String email = jwtTokenProvider.getEmailFromToken(token); + User user = userRepository.findByEmail(email).orElseThrow(() -> new IllegalArgumentException("User not found")); + request.setAttribute("user", user); + } + return true; + } +} diff --git a/src/main/java/apptive/devlog/global/security/jwt/JwtTokenProvider.java b/src/main/java/apptive/devlog/global/security/jwt/JwtTokenProvider.java index a004ec6..bbcc0d8 100644 --- a/src/main/java/apptive/devlog/global/security/jwt/JwtTokenProvider.java +++ b/src/main/java/apptive/devlog/global/security/jwt/JwtTokenProvider.java @@ -3,6 +3,7 @@ import apptive.devlog.infrastructure.redis.repository.RedisRepository; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Value; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -81,4 +82,12 @@ public boolean validateRefreshToken(String token) { public String getEmailFromToken(String token) { return jwtParser.parseClaimsJws(token).getBody().getSubject(); } + + public String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } } diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index 3059a9e..fa0bd06 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -1,7 +1,7 @@ # =============================== # DB CONFIG # =============================== -spring.datasource.url=jdbc:mysql://localhost:3306/devlog?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 +spring.datasource.url=jdbc:mysql://localhost:3306/devlog?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.username=root @@ -38,9 +38,10 @@ logging.level.com.apptive.devlog=DEBUG # SWAGGER # =============================== springdoc.api-docs.path=/api-docs -springdoc.swagger-ui.path=/swagger-ui.html +springdoc.swagger-ui.path=/swagger-ui/index.html springdoc.default-consumes-media-type=application/json springdoc.default-produces-media-type=application/json +springdoc.swagger-ui.disable-swagger-default-url=true # =============================== # JWT CONFIG diff --git a/src/test/java/apptive/devlog/domain/comment/controller/CommentControllerTest.java b/src/test/java/apptive/devlog/domain/comment/controller/CommentControllerTest.java new file mode 100644 index 0000000..f80a33c --- /dev/null +++ b/src/test/java/apptive/devlog/domain/comment/controller/CommentControllerTest.java @@ -0,0 +1,4 @@ +package apptive.devlog.domain.comment.controller; + +public class CommentControllerTest { +} diff --git a/src/test/java/apptive/devlog/domain/comment/service/CommentServiceTest.java b/src/test/java/apptive/devlog/domain/comment/service/CommentServiceTest.java new file mode 100644 index 0000000..196b64c --- /dev/null +++ b/src/test/java/apptive/devlog/domain/comment/service/CommentServiceTest.java @@ -0,0 +1,4 @@ +package apptive.devlog.domain.comment.service; + +public class CommentServiceTest { +} diff --git a/src/test/java/apptive/devlog/domain/post/controller/PostControllerTest.java b/src/test/java/apptive/devlog/domain/post/controller/PostControllerTest.java new file mode 100644 index 0000000..4a73cb5 --- /dev/null +++ b/src/test/java/apptive/devlog/domain/post/controller/PostControllerTest.java @@ -0,0 +1,4 @@ +package apptive.devlog.domain.post.controller; + +public class PostControllerTest { +} diff --git a/src/test/java/apptive/devlog/domain/post/service/PostServiceTest.java b/src/test/java/apptive/devlog/domain/post/service/PostServiceTest.java new file mode 100644 index 0000000..456b734 --- /dev/null +++ b/src/test/java/apptive/devlog/domain/post/service/PostServiceTest.java @@ -0,0 +1,4 @@ +package apptive.devlog.domain.post.service; + +public class PostServiceTest { +} From 2a3a0c854c5ba8e8d07e77d1a3e70cbd36c09c3a Mon Sep 17 00:00:00 2001 From: qqwwee771441 <104188251+qqwwee771441@users.noreply.github.com> Date: Thu, 10 Apr 2025 11:18:28 +0900 Subject: [PATCH 08/11] fix: move java files --- .../documentation/{ => domain}/auth/AuthLoginDoc.java | 4 ++-- .../documentation/{ => domain}/auth/AuthLogoutDoc.java | 2 +- .../documentation/{ => domain}/auth/AuthRefreshDoc.java | 4 ++-- .../documentation/{ => domain}/auth/AuthSignupDoc.java | 4 ++-- .../{ => domain}/comment/CommentCreateDoc.java | 2 +- .../{ => domain}/comment/CommentDeleteDoc.java | 2 +- .../{ => domain}/comment/CommentReadDoc.java | 2 +- .../{ => domain}/comment/CommentUpdateDoc.java | 2 +- .../documentation/{ => domain}/post/PostCreateDoc.java | 2 +- .../documentation/{ => domain}/post/PostDeleteDoc.java | 2 +- .../documentation/{ => domain}/post/PostReadDoc.java | 2 +- .../documentation/{ => domain}/post/PostUpdateDoc.java | 2 +- .../documentation/{ => domain}/user/UserProfileDoc.java | 2 +- .../user}/wrapper/UserLoginResponseWrapper.java | 2 +- .../user}/wrapper/UserRefreshResponseWrapper.java | 2 +- .../user}/wrapper/UserSignupResponseWrapper.java | 2 +- .../devlog/domain/auth/controller/AuthController.java | 8 ++++---- .../domain/comment/controller/CommentController.java | 8 ++++---- .../devlog/domain/post/controller/PostController.java | 8 ++++---- .../devlog/domain/user/controller/UserController.java | 2 +- 20 files changed, 32 insertions(+), 32 deletions(-) rename src/main/java/apptive/devlog/documentation/{ => domain}/auth/AuthLoginDoc.java (86%) rename src/main/java/apptive/devlog/documentation/{ => domain}/auth/AuthLogoutDoc.java (92%) rename src/main/java/apptive/devlog/documentation/{ => domain}/auth/AuthRefreshDoc.java (87%) rename src/main/java/apptive/devlog/documentation/{ => domain}/auth/AuthSignupDoc.java (87%) rename src/main/java/apptive/devlog/documentation/{ => domain}/comment/CommentCreateDoc.java (84%) rename src/main/java/apptive/devlog/documentation/{ => domain}/comment/CommentDeleteDoc.java (84%) rename src/main/java/apptive/devlog/documentation/{ => domain}/comment/CommentReadDoc.java (78%) rename src/main/java/apptive/devlog/documentation/{ => domain}/comment/CommentUpdateDoc.java (84%) rename src/main/java/apptive/devlog/documentation/{ => domain}/post/PostCreateDoc.java (82%) rename src/main/java/apptive/devlog/documentation/{ => domain}/post/PostDeleteDoc.java (82%) rename src/main/java/apptive/devlog/documentation/{ => domain}/post/PostReadDoc.java (71%) rename src/main/java/apptive/devlog/documentation/{ => domain}/post/PostUpdateDoc.java (82%) rename src/main/java/apptive/devlog/documentation/{ => domain}/user/UserProfileDoc.java (95%) rename src/main/java/apptive/devlog/documentation/{ => domain/user}/wrapper/UserLoginResponseWrapper.java (83%) rename src/main/java/apptive/devlog/documentation/{ => domain/user}/wrapper/UserRefreshResponseWrapper.java (84%) rename src/main/java/apptive/devlog/documentation/{ => domain/user}/wrapper/UserSignupResponseWrapper.java (84%) diff --git a/src/main/java/apptive/devlog/documentation/auth/AuthLoginDoc.java b/src/main/java/apptive/devlog/documentation/domain/auth/AuthLoginDoc.java similarity index 86% rename from src/main/java/apptive/devlog/documentation/auth/AuthLoginDoc.java rename to src/main/java/apptive/devlog/documentation/domain/auth/AuthLoginDoc.java index b533c83..015ebf9 100644 --- a/src/main/java/apptive/devlog/documentation/auth/AuthLoginDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/auth/AuthLoginDoc.java @@ -1,6 +1,6 @@ -package apptive.devlog.documentation.auth; +package apptive.devlog.documentation.domain.auth; -import apptive.devlog.documentation.wrapper.UserLoginResponseWrapper; +import apptive.devlog.documentation.domain.user.wrapper.UserLoginResponseWrapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/apptive/devlog/documentation/auth/AuthLogoutDoc.java b/src/main/java/apptive/devlog/documentation/domain/auth/AuthLogoutDoc.java similarity index 92% rename from src/main/java/apptive/devlog/documentation/auth/AuthLogoutDoc.java rename to src/main/java/apptive/devlog/documentation/domain/auth/AuthLogoutDoc.java index 6388b8a..98be83a 100644 --- a/src/main/java/apptive/devlog/documentation/auth/AuthLogoutDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/auth/AuthLogoutDoc.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.auth; +package apptive.devlog.documentation.domain.auth; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; diff --git a/src/main/java/apptive/devlog/documentation/auth/AuthRefreshDoc.java b/src/main/java/apptive/devlog/documentation/domain/auth/AuthRefreshDoc.java similarity index 87% rename from src/main/java/apptive/devlog/documentation/auth/AuthRefreshDoc.java rename to src/main/java/apptive/devlog/documentation/domain/auth/AuthRefreshDoc.java index 595ed85..6ec6ea4 100644 --- a/src/main/java/apptive/devlog/documentation/auth/AuthRefreshDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/auth/AuthRefreshDoc.java @@ -1,6 +1,6 @@ -package apptive.devlog.documentation.auth; +package apptive.devlog.documentation.domain.auth; -import apptive.devlog.documentation.wrapper.UserRefreshResponseWrapper; +import apptive.devlog.documentation.domain.user.wrapper.UserRefreshResponseWrapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/apptive/devlog/documentation/auth/AuthSignupDoc.java b/src/main/java/apptive/devlog/documentation/domain/auth/AuthSignupDoc.java similarity index 87% rename from src/main/java/apptive/devlog/documentation/auth/AuthSignupDoc.java rename to src/main/java/apptive/devlog/documentation/domain/auth/AuthSignupDoc.java index d7e2f3a..9e38bb3 100644 --- a/src/main/java/apptive/devlog/documentation/auth/AuthSignupDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/auth/AuthSignupDoc.java @@ -1,7 +1,7 @@ -package apptive.devlog.documentation.auth; +package apptive.devlog.documentation.domain.auth; import apptive.devlog.common.response.success.CommonResponse; -import apptive.devlog.documentation.wrapper.UserSignupResponseWrapper; +import apptive.devlog.documentation.domain.user.wrapper.UserSignupResponseWrapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/apptive/devlog/documentation/comment/CommentCreateDoc.java b/src/main/java/apptive/devlog/documentation/domain/comment/CommentCreateDoc.java similarity index 84% rename from src/main/java/apptive/devlog/documentation/comment/CommentCreateDoc.java rename to src/main/java/apptive/devlog/documentation/domain/comment/CommentCreateDoc.java index 947ee8d..0a57710 100644 --- a/src/main/java/apptive/devlog/documentation/comment/CommentCreateDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/comment/CommentCreateDoc.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.comment; +package apptive.devlog.documentation.domain.comment; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; diff --git a/src/main/java/apptive/devlog/documentation/comment/CommentDeleteDoc.java b/src/main/java/apptive/devlog/documentation/domain/comment/CommentDeleteDoc.java similarity index 84% rename from src/main/java/apptive/devlog/documentation/comment/CommentDeleteDoc.java rename to src/main/java/apptive/devlog/documentation/domain/comment/CommentDeleteDoc.java index 9d8310a..5cf7d9f 100644 --- a/src/main/java/apptive/devlog/documentation/comment/CommentDeleteDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/comment/CommentDeleteDoc.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.comment; +package apptive.devlog.documentation.domain.comment; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; diff --git a/src/main/java/apptive/devlog/documentation/comment/CommentReadDoc.java b/src/main/java/apptive/devlog/documentation/domain/comment/CommentReadDoc.java similarity index 78% rename from src/main/java/apptive/devlog/documentation/comment/CommentReadDoc.java rename to src/main/java/apptive/devlog/documentation/domain/comment/CommentReadDoc.java index 3799153..6c738a6 100644 --- a/src/main/java/apptive/devlog/documentation/comment/CommentReadDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/comment/CommentReadDoc.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.comment; +package apptive.devlog.documentation.domain.comment; import io.swagger.v3.oas.annotations.Operation; diff --git a/src/main/java/apptive/devlog/documentation/comment/CommentUpdateDoc.java b/src/main/java/apptive/devlog/documentation/domain/comment/CommentUpdateDoc.java similarity index 84% rename from src/main/java/apptive/devlog/documentation/comment/CommentUpdateDoc.java rename to src/main/java/apptive/devlog/documentation/domain/comment/CommentUpdateDoc.java index c51d8c9..d5bd083 100644 --- a/src/main/java/apptive/devlog/documentation/comment/CommentUpdateDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/comment/CommentUpdateDoc.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.comment; +package apptive.devlog.documentation.domain.comment; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; diff --git a/src/main/java/apptive/devlog/documentation/post/PostCreateDoc.java b/src/main/java/apptive/devlog/documentation/domain/post/PostCreateDoc.java similarity index 82% rename from src/main/java/apptive/devlog/documentation/post/PostCreateDoc.java rename to src/main/java/apptive/devlog/documentation/domain/post/PostCreateDoc.java index 48ad831..9cb1b8e 100644 --- a/src/main/java/apptive/devlog/documentation/post/PostCreateDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/post/PostCreateDoc.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.post; +package apptive.devlog.documentation.domain.post; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; diff --git a/src/main/java/apptive/devlog/documentation/post/PostDeleteDoc.java b/src/main/java/apptive/devlog/documentation/domain/post/PostDeleteDoc.java similarity index 82% rename from src/main/java/apptive/devlog/documentation/post/PostDeleteDoc.java rename to src/main/java/apptive/devlog/documentation/domain/post/PostDeleteDoc.java index 6ec5f07..a8f9699 100644 --- a/src/main/java/apptive/devlog/documentation/post/PostDeleteDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/post/PostDeleteDoc.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.post; +package apptive.devlog.documentation.domain.post; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; diff --git a/src/main/java/apptive/devlog/documentation/post/PostReadDoc.java b/src/main/java/apptive/devlog/documentation/domain/post/PostReadDoc.java similarity index 71% rename from src/main/java/apptive/devlog/documentation/post/PostReadDoc.java rename to src/main/java/apptive/devlog/documentation/domain/post/PostReadDoc.java index 2deae5a..35e9669 100644 --- a/src/main/java/apptive/devlog/documentation/post/PostReadDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/post/PostReadDoc.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.post; +package apptive.devlog.documentation.domain.post; import io.swagger.v3.oas.annotations.Operation; diff --git a/src/main/java/apptive/devlog/documentation/post/PostUpdateDoc.java b/src/main/java/apptive/devlog/documentation/domain/post/PostUpdateDoc.java similarity index 82% rename from src/main/java/apptive/devlog/documentation/post/PostUpdateDoc.java rename to src/main/java/apptive/devlog/documentation/domain/post/PostUpdateDoc.java index 0d0436b..8f714f2 100644 --- a/src/main/java/apptive/devlog/documentation/post/PostUpdateDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/post/PostUpdateDoc.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.post; +package apptive.devlog.documentation.domain.post; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; diff --git a/src/main/java/apptive/devlog/documentation/user/UserProfileDoc.java b/src/main/java/apptive/devlog/documentation/domain/user/UserProfileDoc.java similarity index 95% rename from src/main/java/apptive/devlog/documentation/user/UserProfileDoc.java rename to src/main/java/apptive/devlog/documentation/domain/user/UserProfileDoc.java index fe6dd58..13424f9 100644 --- a/src/main/java/apptive/devlog/documentation/user/UserProfileDoc.java +++ b/src/main/java/apptive/devlog/documentation/domain/user/UserProfileDoc.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.user; +package apptive.devlog.documentation.domain.user; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; diff --git a/src/main/java/apptive/devlog/documentation/wrapper/UserLoginResponseWrapper.java b/src/main/java/apptive/devlog/documentation/domain/user/wrapper/UserLoginResponseWrapper.java similarity index 83% rename from src/main/java/apptive/devlog/documentation/wrapper/UserLoginResponseWrapper.java rename to src/main/java/apptive/devlog/documentation/domain/user/wrapper/UserLoginResponseWrapper.java index cc3c6c7..b2c775e 100644 --- a/src/main/java/apptive/devlog/documentation/wrapper/UserLoginResponseWrapper.java +++ b/src/main/java/apptive/devlog/documentation/domain/user/wrapper/UserLoginResponseWrapper.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.wrapper; +package apptive.devlog.documentation.domain.user.wrapper; import apptive.devlog.common.response.success.CommonResponse; import apptive.devlog.domain.auth.dto.UserLoginResponseDto; diff --git a/src/main/java/apptive/devlog/documentation/wrapper/UserRefreshResponseWrapper.java b/src/main/java/apptive/devlog/documentation/domain/user/wrapper/UserRefreshResponseWrapper.java similarity index 84% rename from src/main/java/apptive/devlog/documentation/wrapper/UserRefreshResponseWrapper.java rename to src/main/java/apptive/devlog/documentation/domain/user/wrapper/UserRefreshResponseWrapper.java index 18ce427..1372552 100644 --- a/src/main/java/apptive/devlog/documentation/wrapper/UserRefreshResponseWrapper.java +++ b/src/main/java/apptive/devlog/documentation/domain/user/wrapper/UserRefreshResponseWrapper.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.wrapper; +package apptive.devlog.documentation.domain.user.wrapper; import apptive.devlog.common.response.success.CommonResponse; import apptive.devlog.domain.auth.dto.UserRefreshResponseDto; diff --git a/src/main/java/apptive/devlog/documentation/wrapper/UserSignupResponseWrapper.java b/src/main/java/apptive/devlog/documentation/domain/user/wrapper/UserSignupResponseWrapper.java similarity index 84% rename from src/main/java/apptive/devlog/documentation/wrapper/UserSignupResponseWrapper.java rename to src/main/java/apptive/devlog/documentation/domain/user/wrapper/UserSignupResponseWrapper.java index 5c43129..fb27e00 100644 --- a/src/main/java/apptive/devlog/documentation/wrapper/UserSignupResponseWrapper.java +++ b/src/main/java/apptive/devlog/documentation/domain/user/wrapper/UserSignupResponseWrapper.java @@ -1,4 +1,4 @@ -package apptive.devlog.documentation.wrapper; +package apptive.devlog.documentation.domain.user.wrapper; import apptive.devlog.common.response.success.CommonResponse; import apptive.devlog.domain.auth.dto.UserSignupResponseDto; diff --git a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java index 2dee54d..e950b7f 100644 --- a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java +++ b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java @@ -1,10 +1,10 @@ package apptive.devlog.domain.auth.controller; import apptive.devlog.common.response.success.CommonResponse; -import apptive.devlog.documentation.auth.AuthLoginDoc; -import apptive.devlog.documentation.auth.AuthLogoutDoc; -import apptive.devlog.documentation.auth.AuthRefreshDoc; -import apptive.devlog.documentation.auth.AuthSignupDoc; +import apptive.devlog.documentation.domain.auth.AuthLoginDoc; +import apptive.devlog.documentation.domain.auth.AuthLogoutDoc; +import apptive.devlog.documentation.domain.auth.AuthRefreshDoc; +import apptive.devlog.documentation.domain.auth.AuthSignupDoc; import apptive.devlog.documentation.tags.AuthDocumentation; import apptive.devlog.domain.auth.dto.*; import apptive.devlog.domain.auth.service.AuthService; diff --git a/src/main/java/apptive/devlog/domain/comment/controller/CommentController.java b/src/main/java/apptive/devlog/domain/comment/controller/CommentController.java index 81660cb..73fc66b 100644 --- a/src/main/java/apptive/devlog/domain/comment/controller/CommentController.java +++ b/src/main/java/apptive/devlog/domain/comment/controller/CommentController.java @@ -1,10 +1,10 @@ package apptive.devlog.domain.comment.controller; import apptive.devlog.common.response.success.CommonResponse; -import apptive.devlog.documentation.comment.CommentCreateDoc; -import apptive.devlog.documentation.comment.CommentDeleteDoc; -import apptive.devlog.documentation.comment.CommentReadDoc; -import apptive.devlog.documentation.comment.CommentUpdateDoc; +import apptive.devlog.documentation.domain.comment.CommentCreateDoc; +import apptive.devlog.documentation.domain.comment.CommentDeleteDoc; +import apptive.devlog.documentation.domain.comment.CommentReadDoc; +import apptive.devlog.documentation.domain.comment.CommentUpdateDoc; import apptive.devlog.documentation.tags.CommentDocumentation; import apptive.devlog.domain.comment.dto.CommentDto; import apptive.devlog.domain.comment.service.CommentService; diff --git a/src/main/java/apptive/devlog/domain/post/controller/PostController.java b/src/main/java/apptive/devlog/domain/post/controller/PostController.java index 40177ee..b8f015d 100644 --- a/src/main/java/apptive/devlog/domain/post/controller/PostController.java +++ b/src/main/java/apptive/devlog/domain/post/controller/PostController.java @@ -1,10 +1,10 @@ package apptive.devlog.domain.post.controller; import apptive.devlog.common.response.success.CommonResponse; -import apptive.devlog.documentation.post.PostCreateDoc; -import apptive.devlog.documentation.post.PostDeleteDoc; -import apptive.devlog.documentation.post.PostReadDoc; -import apptive.devlog.documentation.post.PostUpdateDoc; +import apptive.devlog.documentation.domain.post.PostCreateDoc; +import apptive.devlog.documentation.domain.post.PostDeleteDoc; +import apptive.devlog.documentation.domain.post.PostReadDoc; +import apptive.devlog.documentation.domain.post.PostUpdateDoc; import apptive.devlog.documentation.tags.PostDocumentation; import apptive.devlog.domain.post.dto.PostDto; import apptive.devlog.domain.post.service.PostService; diff --git a/src/main/java/apptive/devlog/domain/user/controller/UserController.java b/src/main/java/apptive/devlog/domain/user/controller/UserController.java index aa8020e..fe23874 100644 --- a/src/main/java/apptive/devlog/domain/user/controller/UserController.java +++ b/src/main/java/apptive/devlog/domain/user/controller/UserController.java @@ -2,7 +2,7 @@ import apptive.devlog.common.response.success.CommonResponse; import apptive.devlog.documentation.tags.UserDocumentation; -import apptive.devlog.documentation.user.UserProfileDoc; +import apptive.devlog.documentation.domain.user.UserProfileDoc; import apptive.devlog.domain.user.dto.UserProfileRequestDto; import apptive.devlog.domain.user.dto.UserProfileResponseDto; import apptive.devlog.domain.user.service.UserService; From 3c010fcce05ea84115b487418ae2aa13f569e37d Mon Sep 17 00:00:00 2001 From: qqwwee771441 <104188251+qqwwee771441@users.noreply.github.com> Date: Fri, 11 Apr 2025 19:21:07 +0900 Subject: [PATCH 09/11] feat: AuthControllerTest.java --- .gitignore | 1 + build.gradle | 1 + .../auth/controller/AuthController.java | 22 ++-- .../domain/auth/dto/UserLogoutRequestDto.java | 2 + .../auth/dto/UserRefreshRequestDto.java | 2 + .../comment/service/CommentService.java | 3 +- .../resolver/InjectTokenArgumentResolver.java | 6 +- .../JwtAuthenticationInterceptor.java | 2 + .../AuthControllerIntegrationTest.java | 97 ++++++++++++++++ .../auth/controller/AuthControllerTest.java | 108 ++++++++++++++++++ .../resources/application-test.properties | 89 +++++++++++++++ src/test/resources/application.properties | 10 ++ 12 files changed, 328 insertions(+), 15 deletions(-) create mode 100644 src/test/java/apptive/devlog/domain/auth/controller/AuthControllerIntegrationTest.java create mode 100644 src/test/resources/application-test.properties create mode 100644 src/test/resources/application.properties diff --git a/.gitignore b/.gitignore index fce8590..38356f8 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ out/ .vscode/ src/main/resources/application-secret.properties +/src/test/resources/application-secret.properties diff --git a/build.gradle b/build.gradle index e8fecc6..39528ee 100644 --- a/build.gradle +++ b/build.gradle @@ -51,6 +51,7 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java index e950b7f..2af8f4f 100644 --- a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java +++ b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java @@ -10,12 +10,14 @@ import apptive.devlog.domain.auth.service.AuthService; import apptive.devlog.global.annotation.InjectToken; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; +@Slf4j @RestController @RequestMapping("/auth") @RequiredArgsConstructor @@ -24,29 +26,33 @@ public class AuthController implements AuthDocumentation { @AuthSignupDoc @PostMapping("/signup") - public ResponseEntity> signup(@Valid @RequestBody UserSignupRequestDto requestDto) { + @ResponseStatus(HttpStatus.CREATED) + public CommonResponse signup(@Valid @RequestBody UserSignupRequestDto requestDto) { UserSignupResponseDto responseDto = authService.signup(requestDto); - return ResponseEntity.status(HttpStatus.CREATED).body(CommonResponse.created(responseDto)); + return CommonResponse.created(responseDto); } @AuthLoginDoc @PostMapping("/login") - public ResponseEntity> login(@Valid @RequestBody UserLoginRequestDto requestDto) { + @ResponseStatus(HttpStatus.OK) + public CommonResponse login(@Valid @RequestBody UserLoginRequestDto requestDto) { UserLoginResponseDto responseDto = authService.login(requestDto); - return ResponseEntity.ok(CommonResponse.ok(responseDto)); + return CommonResponse.ok(responseDto); } @AuthRefreshDoc @PostMapping("/refresh") - public ResponseEntity> refresh(@Valid @InjectToken UserRefreshRequestDto requestDto) { + @ResponseStatus(HttpStatus.OK) + public CommonResponse refresh(@Valid @InjectToken UserRefreshRequestDto requestDto) { UserRefreshResponseDto responseDto = authService.refresh(requestDto); - return ResponseEntity.ok(CommonResponse.ok(responseDto)); + return CommonResponse.ok(responseDto); } @AuthLogoutDoc @PostMapping("/logout") - public ResponseEntity> logout(@Valid @InjectToken UserLogoutRequestDto requestDto) { + @ResponseStatus(HttpStatus.NO_CONTENT) + public CommonResponse logout(@Valid @InjectToken UserLogoutRequestDto requestDto) { authService.logout(requestDto); - return ResponseEntity.status(HttpStatus.NO_CONTENT).body(CommonResponse.noContent()); + return CommonResponse.noContent(); } } diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserLogoutRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserLogoutRequestDto.java index aebbaad..f2809d2 100644 --- a/src/main/java/apptive/devlog/domain/auth/dto/UserLogoutRequestDto.java +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserLogoutRequestDto.java @@ -2,11 +2,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor +@AllArgsConstructor @Schema(description = "사용자 로그아웃 요청 DTO") public class UserLogoutRequestDto { @Schema(description = "Access Token", example = "eyJhbGciOiJIUzI1NiJ9...") diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshRequestDto.java index b646862..89d5668 100644 --- a/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshRequestDto.java +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserRefreshRequestDto.java @@ -2,11 +2,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor +@AllArgsConstructor @Schema(description = "Access Token과 Refresh Token 재발급 요청 DTO") public class UserRefreshRequestDto { @NotBlank diff --git a/src/main/java/apptive/devlog/domain/comment/service/CommentService.java b/src/main/java/apptive/devlog/domain/comment/service/CommentService.java index c40a411..4901d78 100644 --- a/src/main/java/apptive/devlog/domain/comment/service/CommentService.java +++ b/src/main/java/apptive/devlog/domain/comment/service/CommentService.java @@ -8,7 +8,6 @@ import apptive.devlog.domain.user.entity.User; import apptive.devlog.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; -import org.apache.coyote.BadRequestException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,7 +37,7 @@ public CommentDto.Response create(String postId, CommentDto.Create dto, User aut } } - Comment comment = Comment.builder().text(dto.text()).author(author).parent(parent).build(); + Comment comment = Comment.builder().text(dto.text()).author(author).parent(parent).post(post).build(); commentRepository.save(comment); return toResponse(comment); } diff --git a/src/main/java/apptive/devlog/global/resolver/InjectTokenArgumentResolver.java b/src/main/java/apptive/devlog/global/resolver/InjectTokenArgumentResolver.java index 361f0f7..d87ae3d 100644 --- a/src/main/java/apptive/devlog/global/resolver/InjectTokenArgumentResolver.java +++ b/src/main/java/apptive/devlog/global/resolver/InjectTokenArgumentResolver.java @@ -34,11 +34,7 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public Object resolveArgument(MethodParameter parameter, - ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, - WebDataBinderFactory binderFactory) throws Exception { - + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); byte[] requestBodyBytes = getRequestBodyAsBytes(request); diff --git a/src/main/java/apptive/devlog/global/security/interceptor/JwtAuthenticationInterceptor.java b/src/main/java/apptive/devlog/global/security/interceptor/JwtAuthenticationInterceptor.java index f22d4bb..0888778 100644 --- a/src/main/java/apptive/devlog/global/security/interceptor/JwtAuthenticationInterceptor.java +++ b/src/main/java/apptive/devlog/global/security/interceptor/JwtAuthenticationInterceptor.java @@ -6,9 +6,11 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; +@Slf4j @Component @RequiredArgsConstructor public class JwtAuthenticationInterceptor implements HandlerInterceptor { diff --git a/src/test/java/apptive/devlog/domain/auth/controller/AuthControllerIntegrationTest.java b/src/test/java/apptive/devlog/domain/auth/controller/AuthControllerIntegrationTest.java new file mode 100644 index 0000000..80bc5fd --- /dev/null +++ b/src/test/java/apptive/devlog/domain/auth/controller/AuthControllerIntegrationTest.java @@ -0,0 +1,97 @@ +package apptive.devlog.domain.auth.controller; + +import apptive.devlog.domain.auth.dto.*; +import apptive.devlog.domain.auth.service.AuthService; +import apptive.devlog.domain.user.entity.User; +import apptive.devlog.domain.user.enums.Gender; +import apptive.devlog.domain.user.enums.Provider; +import apptive.devlog.domain.user.enums.Role; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +public class AuthControllerIntegrationTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private AuthService authService; + + @Nested + @DisplayName("회원가입 테스트") + class SignupTests { + @Test + @DisplayName("회원가입 성공") + void signupSuccess() throws Exception { + UserSignupRequestDto requestDto = new UserSignupRequestDto("test@example.com", "Password123!", "최광진", "qqwwee41", LocalDate.of(2000, 9, 26), Gender.MALE); + UserSignupResponseDto responseDto = new UserSignupResponseDto(new User("test@example.com", "최광진", Provider.LOCAL, Role.USER)); + + when(authService.signup(any(UserSignupRequestDto.class))).thenReturn(responseDto); + + mockMvc.perform(post("/auth/signup").with(csrf()).contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(requestDto)).characterEncoding("UTF-8")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.status").value(201)) + .andExpect(jsonPath("$.data.email").value("test@example.com")); + } + } + + @Nested + @DisplayName("로그인 테스트") + class LoginTests { + @Test + void loginSuccess() throws Exception { + UserLoginRequestDto requestDto = new UserLoginRequestDto("test@example.com", "Password123!"); + UserLoginResponseDto responseDto = new UserLoginResponseDto("access-token", "refresh-token"); + + when(authService.login(any(UserLoginRequestDto.class))).thenReturn(responseDto); + + mockMvc.perform(post("/auth/login").with(csrf()).contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(requestDto)).characterEncoding("UTF-8")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.data.accessToken").value("access-token")) + .andExpect(jsonPath("$.data.refreshToken").value("refresh-token")); + } + } + + @Nested + @DisplayName("리프레시 테스트") + class RefreshTests { + @Test + void refreshSuccess() throws Exception { + UserRefreshRequestDto requestDto = new UserRefreshRequestDto("access-token", "refresh-token"); + UserRefreshResponseDto responseDto = new UserRefreshResponseDto("access-token", "refresh-token"); + + } + } + + @Nested + @DisplayName("로그아웃 테스트") + class LogoutTests { + @Test + void logoutSuccess() throws Exception { + UserLogoutRequestDto requestDto = new UserLogoutRequestDto("access-token", "refresh-token"); + + } + } +} diff --git a/src/test/java/apptive/devlog/domain/auth/controller/AuthControllerTest.java b/src/test/java/apptive/devlog/domain/auth/controller/AuthControllerTest.java index 6428236..33a04b3 100644 --- a/src/test/java/apptive/devlog/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/apptive/devlog/domain/auth/controller/AuthControllerTest.java @@ -1,4 +1,112 @@ package apptive.devlog.domain.auth.controller; +import apptive.devlog.domain.auth.dto.*; +import apptive.devlog.domain.auth.service.AuthService; +import apptive.devlog.domain.user.entity.User; +import apptive.devlog.domain.user.enums.Gender; +import apptive.devlog.domain.user.enums.Provider; +import apptive.devlog.domain.user.enums.Role; +import apptive.devlog.global.resolver.InjectTokenArgumentResolver; +import apptive.devlog.global.security.interceptor.JwtAuthenticationInterceptor; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.time.LocalDate; + +@WithMockUser +@ExtendWith(MockitoExtension.class) +@WebMvcTest(AuthController.class) +@ActiveProfiles("test") public class AuthControllerTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private AuthService authService; + + @MockitoBean + private JwtAuthenticationInterceptor jwtAuthenticationInterceptor; + + @MockitoBean + private InjectTokenArgumentResolver injectTokenArgumentResolver; + + @Nested + @DisplayName("회원가입 테스트") + public class SignupTests { + @Test + @DisplayName("회원가입 성공") + public void signupSuccess() throws Exception { + UserSignupRequestDto requestDto = new UserSignupRequestDto("test@example.com", "Password123!", "choi", "test", LocalDate.of(2000, 9, 26), Gender.MALE); + UserSignupResponseDto responseDto = new UserSignupResponseDto(new User("test@example.com", "choi", Provider.LOCAL, Role.USER)); + + when(jwtAuthenticationInterceptor.preHandle(any(), any(), any())).thenReturn(true); + when(authService.signup(any(UserSignupRequestDto.class))).thenReturn(responseDto); + + mockMvc.perform(post("/auth/signup").with(csrf()).contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(requestDto)).characterEncoding("UTF-8")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.status").value(201)) + .andExpect(jsonPath("$.data.email").value("test@example.com"));; + } + } + + @Nested + @DisplayName("로그인 테스트") + class LoginTests { + @Test + void loginSuccess() throws Exception { + UserLoginRequestDto requestDto = new UserLoginRequestDto("test@example.com", "Password123!"); + UserLoginResponseDto responseDto = new UserLoginResponseDto("access-token", "refresh-token"); + + when(jwtAuthenticationInterceptor.preHandle(any(), any(), any())).thenReturn(true); + when(authService.login(any(UserLoginRequestDto.class))).thenReturn(responseDto); + + mockMvc.perform(post("/auth/login").with(csrf()).contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(requestDto)).characterEncoding("UTF-8")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.data.accessToken").value("access-token")) + .andExpect(jsonPath("$.data.refreshToken").value("refresh-token")); + } + } + + @Nested + @DisplayName("리프레시 테스트") + class RefreshTests { + @Test + void refreshSuccess() throws Exception { + UserRefreshRequestDto requestDto = new UserRefreshRequestDto("access-token", "refresh-token"); + UserRefreshResponseDto responseDto = new UserRefreshResponseDto("access-token", "refresh-token"); + + } + } + + @Nested + @DisplayName("로그아웃 테스트") + class LogoutTests { + @Test + void logoutSuccess() throws Exception { + UserLogoutRequestDto requestDto = new UserLogoutRequestDto("access-token", "refresh-token"); + + } + } } diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..fa0bd06 --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,89 @@ +# =============================== +# DB CONFIG +# =============================== +spring.datasource.url=jdbc:mysql://localhost:3306/devlog?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.username=root + +# =============================== +# JPA CONFIG +# =============================== +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=create +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.show_sql=true +spring.jpa.open-in-view=false +spring.jpa.show-sql=true + +# =============================== +# SERVER CONFIG +# =============================== +server.port=8080 +server.ssl.enabled=false + +# =============================== +# REDIS CONFIG +# =============================== +spring.data.redis.host=localhost +spring.data.redis.port=6379 + +# =============================== +# LOGGING +# =============================== +logging.level.org.springframework.security=DEBUG +logging.level.com.apptive.devlog=DEBUG + +# =============================== +# SWAGGER +# =============================== +springdoc.api-docs.path=/api-docs +springdoc.swagger-ui.path=/swagger-ui/index.html +springdoc.default-consumes-media-type=application/json +springdoc.default-produces-media-type=application/json +springdoc.swagger-ui.disable-swagger-default-url=true + +# =============================== +# JWT CONFIG +# =============================== +jwt.access-token-expiration=3600000 +jwt.refresh-token-expiration=1209600000 + +# =============================== +# OAUTH2 CONFIG - GOOGLE +# =============================== +spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.google.scope=profile,email +spring.security.oauth2.client.registration.google.client-name=Google +spring.security.oauth2.client.registration.google.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth +spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token +spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo +spring.security.oauth2.client.provider.google.user-name-attribute=sub + +# =============================== +# OAUTH2 CONFIG - NAVER +# =============================== +spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.naver.scope=name,email +spring.security.oauth2.client.registration.naver.client-name=Naver +spring.security.oauth2.client.registration.naver.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize +spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token +spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me +spring.security.oauth2.client.provider.naver.user-name-attribute=response + +# =============================== +# OAUTH2 CONFIG - KAKAO +# =============================== +spring.security.oauth2.client.registration.kakao.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email +spring.security.oauth2.client.registration.kakao.client-name=Kakao +spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize +spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token +spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me +spring.security.oauth2.client.provider.kakao.user-name-attribute=id diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..16e5fac --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,10 @@ +# =============================== +# APPLICATION CONFIG +# =============================== +spring.application.name=devlog + +# =============================== +# PROFILE CONFIG +# =============================== +spring.profiles.active=test +spring.profiles.include=secret From de916b7ad1df37a8d3bf14d714d4769f0d9584e3 Mon Sep 17 00:00:00 2001 From: qqwwee771441 <104188251+qqwwee771441@users.noreply.github.com> Date: Sat, 12 Apr 2025 13:36:42 +0900 Subject: [PATCH 10/11] feat: OAuth2SuccessHandlerForUnity.java --- .gitignore | 4 +- Dockerfile | 11 +++ my.cnf | 33 +++++++ .../auth/controller/AuthController.java | 2 +- .../domain/auth/service/AuthService.java | 2 + .../handler/OAuth2SuccessHandlerForUnity.java | 36 ++++++++ .../security/config/SecurityConfig.java | 4 +- .../resources/application-release.properties | 89 +++++++++++++++++++ src/main/resources/application.properties | 2 +- start.sh | 13 +++ 10 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 Dockerfile create mode 100644 my.cnf create mode 100644 src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandlerForUnity.java create mode 100644 src/main/resources/application-release.properties create mode 100644 start.sh diff --git a/.gitignore b/.gitignore index 38356f8..a8f4b52 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,6 @@ out/ .vscode/ src/main/resources/application-secret.properties -/src/test/resources/application-secret.properties +src/main/resources/keystore.p12 +docker-compose.yml +src/test/resources/application-secret.properties diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..04728f5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM openjdk:21-jdk-slim + +WORKDIR /app + +ENV TZ=Asia/Seoul + +# JAR 복사 +COPY ./build/libs/devlog-0.0.1-SNAPSHOT.jar app.jar + +# 실행 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/my.cnf b/my.cnf new file mode 100644 index 0000000..51c31d6 --- /dev/null +++ b/my.cnf @@ -0,0 +1,33 @@ +# For advice on how to change settings please see +# http://dev.mysql.com/doc/refman/9.2/en/server-configuration-defaults.html + +[mysqld] +bind-address=0.0.0.0 +# +# Remove leading # and set to the amount of RAM for the most important data +# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. +# innodb_buffer_pool_size = 128M +# +# Remove leading # to turn on a very important data integrity option: logging +# changes to the binary log between backups. +# log_bin +# +# Remove leading # to set options mainly useful for reporting servers. +# The server defaults are faster for transactions and fast SELECTs. +# Adjust sizes as needed, experiment to find the optimal values. +# join_buffer_size = 128M +# sort_buffer_size = 2M +# read_rnd_buffer_size = 2M + +host-cache-size=0 +skip-name-resolve +datadir=/var/lib/mysql +socket=/var/run/mysqld/mysqld.sock +secure-file-priv=/var/lib/mysql-files +user=mysql + +pid-file=/var/run/mysqld/mysqld.pid +[client] +socket=/var/run/mysqld/mysqld.sock + +!includedir /etc/mysql/conf.d/ diff --git a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java index 2af8f4f..62d621c 100644 --- a/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java +++ b/src/main/java/apptive/devlog/domain/auth/controller/AuthController.java @@ -50,7 +50,7 @@ public CommonResponse refresh(@Valid @InjectToken UserRe @AuthLogoutDoc @PostMapping("/logout") - @ResponseStatus(HttpStatus.NO_CONTENT) + @ResponseStatus(HttpStatus.OK) public CommonResponse logout(@Valid @InjectToken UserLogoutRequestDto requestDto) { authService.logout(requestDto); return CommonResponse.noContent(); diff --git a/src/main/java/apptive/devlog/domain/auth/service/AuthService.java b/src/main/java/apptive/devlog/domain/auth/service/AuthService.java index 2d31c00..0681b92 100644 --- a/src/main/java/apptive/devlog/domain/auth/service/AuthService.java +++ b/src/main/java/apptive/devlog/domain/auth/service/AuthService.java @@ -9,9 +9,11 @@ import apptive.devlog.domain.user.repository.UserRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +@Slf4j @Service @RequiredArgsConstructor public class AuthService { diff --git a/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandlerForUnity.java b/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandlerForUnity.java new file mode 100644 index 0000000..3e32654 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/oauth2/handler/OAuth2SuccessHandlerForUnity.java @@ -0,0 +1,36 @@ +package apptive.devlog.domain.oauth2.handler; + +import apptive.devlog.global.security.jwt.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +@Component +@RequiredArgsConstructor +public class OAuth2SuccessHandlerForUnity extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); + String email = oAuth2User.getAttribute("email"); + + String accessToken = jwtTokenProvider.generateAccessToken(email); + String refreshToken = jwtTokenProvider.generateRefreshToken(email); + + String redirectUrl = "http://localhost:5000/callback" + + "?accessToken=" + URLEncoder.encode(accessToken, StandardCharsets.UTF_8) + + "&refreshToken=" + URLEncoder.encode(refreshToken, StandardCharsets.UTF_8); + + getRedirectStrategy().sendRedirect(request, response, redirectUrl); + } +} diff --git a/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java b/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java index 116f3c7..26c984d 100644 --- a/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java +++ b/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java @@ -1,5 +1,6 @@ package apptive.devlog.global.security.config; +import apptive.devlog.domain.oauth2.handler.OAuth2SuccessHandlerForUnity; import apptive.devlog.global.security.filter.JwtAuthenticationFilter; import apptive.devlog.domain.oauth2.handler.OAuth2FailureHandler; import apptive.devlog.domain.oauth2.handler.OAuth2SuccessHandler; @@ -31,6 +32,7 @@ public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; private final OAuth2SuccessHandler oAuth2SuccessHandler; private final OAuth2FailureHandler oAuth2FailureHandler; + private final OAuth2SuccessHandlerForUnity oAuth2SuccessHandlerForUnity; @Bean public PasswordEncoder passwordEncoder() { @@ -66,7 +68,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizationEndpoint(endpoint -> endpoint.baseUri("/oauth2/authorization")) .redirectionEndpoint(endpoint -> endpoint.baseUri("/oauth2/callback/*")) .userInfoEndpoint(endpoint -> endpoint.userService(customOAuth2UserService)) - .successHandler(oAuth2SuccessHandler) + .successHandler(oAuth2SuccessHandlerForUnity) .failureHandler(oAuth2FailureHandler) ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) diff --git a/src/main/resources/application-release.properties b/src/main/resources/application-release.properties new file mode 100644 index 0000000..4e5ab82 --- /dev/null +++ b/src/main/resources/application-release.properties @@ -0,0 +1,89 @@ +# =============================== +# DB CONFIG +# =============================== +spring.datasource.url=jdbc:mysql://172.23.171.122:3307/devlog?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.username=root + +# =============================== +# JPA CONFIG +# =============================== +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.show_sql=false +spring.jpa.open-in-view=false +spring.jpa.show-sql=false + +# =============================== +# SERVER CONFIG +# =============================== +server.port=8080 +server.ssl.enabled=false + +# =============================== +# REDIS CONFIG +# =============================== +spring.data.redis.host=devlog-redis +spring.data.redis.port=6379 + +# =============================== +# LOGGING +# =============================== +logging.level.org.springframework.security=INFO +logging.level.com.apptive.devlog=INFO + +# =============================== +# SWAGGER +# =============================== +springdoc.api-docs.path=/api-docs +springdoc.swagger-ui.path=/swagger-ui/index.html +springdoc.default-consumes-media-type=application/json +springdoc.default-produces-media-type=application/json +springdoc.swagger-ui.disable-swagger-default-url=true + +# =============================== +# JWT CONFIG +# =============================== +jwt.access-token-expiration=3600000 +jwt.refresh-token-expiration=1209600000 + +# =============================== +# OAUTH2 CONFIG - GOOGLE +# =============================== +spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.google.scope=profile,email +spring.security.oauth2.client.registration.google.client-name=Google +spring.security.oauth2.client.registration.google.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth +spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token +spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo +spring.security.oauth2.client.provider.google.user-name-attribute=sub + +# =============================== +# OAUTH2 CONFIG - NAVER +# =============================== +spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.naver.scope=name,email +spring.security.oauth2.client.registration.naver.client-name=Naver +spring.security.oauth2.client.registration.naver.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize +spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token +spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me +spring.security.oauth2.client.provider.naver.user-name-attribute=response + +# =============================== +# OAUTH2 CONFIG - KAKAO +# =============================== +spring.security.oauth2.client.registration.kakao.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email +spring.security.oauth2.client.registration.kakao.client-name=Kakao +spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize +spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token +spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me +spring.security.oauth2.client.provider.kakao.user-name-attribute=id diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3210b1a..10dd667 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,5 +6,5 @@ spring.application.name=devlog # =============================== # PROFILE CONFIG # =============================== -spring.profiles.active=local +spring.profiles.active=release spring.profiles.include=secret diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..ab68b32 --- /dev/null +++ b/start.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +echo "🔄 Cleaning and building project..." +./gradlew clean build -x test + +echo "🐳 Stopping and removing existing containers..." +docker-compose down + +echo "🐳 Building Docker images..." +docker-compose build --no-cache + +echo "🚀 Starting containers..." +docker-compose up From 3a3a868919475dd09f7cb59ecc297e32eff0b72b Mon Sep 17 00:00:00 2001 From: qqwwee771441 <104188251+qqwwee771441@users.noreply.github.com> Date: Wed, 30 Apr 2025 19:54:35 +0900 Subject: [PATCH 11/11] feat: deploy --- .gitignore | 2 + app.sh | 3 + build.gradle | 36 +++-- certs/app_certs/ca.pem | 40 +++++ certs/app_certs/client-ca.pem | 40 +++++ certs/app_certs/client-cert.pem | 19 +++ certs/app_certs/client-key-nopass.pem | 28 ++++ certs/app_certs/client-key.pem | 30 ++++ certs/app_certs/client.csr | 16 ++ certs/app_certs/google-fullchain.pem | 89 +++++++++++ certs/app_certs/google-intermediate.pem | 30 ++++ certs/app_certs/google-root.pem | 32 ++++ certs/app_certs/google-server.pem | 27 ++++ certs/app_certs/google.crt | 26 ++++ certs/app_certs/kakao.crt | 36 +++++ certs/app_certs/keystore.p12 | Bin 0 -> 3480 bytes certs/app_certs/keystore.pem | 78 ++++++++++ certs/app_certs/naver.crt | 42 +++++ certs/app_certs/server-cert.pem | 19 +++ certs/app_certs/server-full-chain.pem | 38 +++++ certs/app_certs/server-key.pem | 28 ++++ certs/app_certs/server.csr | 16 ++ certs/app_certs/truststore.p12 | Bin 0 -> 16294 bytes certs/app_certs/truststore.pem | 147 ++++++++++++++++++ certs/ca-key.pem | 30 ++++ certs/ca.pem | 21 +++ certs/ca.srl | 1 + certs/intermediate-key.pem | 30 ++++ certs/intermediate.csr | 16 ++ certs/intermediate.pem | 19 +++ certs/intermediate.srl | 1 + certs/mysql_certs/ca.pem | 40 +++++ certs/mysql_certs/server-cert.pem | 19 +++ certs/mysql_certs/server-full-chain.pem | 38 +++++ certs/mysql_certs/server-key-nopass.pem | 28 ++++ certs/mysql_certs/server-key.pem | 30 ++++ certs/mysql_certs/server.csr | 16 ++ certs/nginx_certs/ca.pem | 40 +++++ certs/nginx_certs/client-ca.pem | 40 +++++ certs/nginx_certs/client-cert.pem | 19 +++ certs/nginx_certs/client-key-nopass.pem | 28 ++++ certs/nginx_certs/client-key.pem | 30 ++++ certs/nginx_certs/client.csr | 16 ++ certs/nginx_certs/dhparam.pem | 8 + certs/nginx_certs/fullchain.pem | 48 ++++++ certs/nginx_certs/keystore.p12 | Bin 0 -> 3532 bytes certs/nginx_certs/privkey.pem | 5 + certs/nginx_certs/server-cert.pem | 20 +++ certs/nginx_certs/server-full-chain.pem | 39 +++++ certs/nginx_certs/server-key-nopass.pem | 28 ++++ certs/nginx_certs/server-key.pem | 30 ++++ certs/nginx_certs/server.csr | 19 +++ certs/nginx_certs/truststore.p12 | Bin 0 -> 1174 bytes certs/redis_certs/ca.pem | 40 +++++ certs/redis_certs/server-cert.pem | 19 +++ certs/redis_certs/server-full-chain.pem | 38 +++++ certs/redis_certs/server-key.pem | 28 ++++ certs/redis_certs/server.csr | 16 ++ cleanup.sh | 72 +++++++++ deploy.sh | 67 ++++++++ my.cnf | 45 +++--- mysql.sh | 3 + nginx.conf | 68 ++++++++ nginx.sh | 3 + ngrok.log | 0 redeploy.sh | 12 ++ redis.conf | 19 +++ redis.sh | 3 + ...nvalidEmailVerificationTokenException.java | 19 +++ .../error/handler/GlobalExceptionHandler.java | 2 +- .../controller/RootRedirectController.java | 13 ++ .../EmailVerificationController.java | 32 ++++ .../domain/auth/dto/EmailSendRequestDto.java | 14 ++ .../auth/dto/EmailVerifyRequestDto.java | 18 +++ .../auth/dto/EmailVerifyResponseDto.java | 13 ++ .../domain/auth/dto/UserSignupRequestDto.java | 8 +- .../repository/VerifiedEmailRepository.java | 48 ++++++ .../domain/auth/service/AuthService.java | 5 + .../service/EmailVerificationService.java | 38 +++++ .../devlog/domain/auth/util/MailSender.java | 41 +++++ .../domain/auth/util/RandomCodeGenerator.java | 11 ++ .../devlog/domain/post/dto/PostDto.java | 1 - .../devlog/domain/post/entity/Post.java | 1 + .../devlog/global/config/MailConfig.java | 40 +++++ .../security/config/SecurityConfig.java | 2 +- .../redis/config/RedisConfig.java | 39 ----- .../config/RedisJedisMutualTlsConfig.java | 94 +++++++++++ .../redis/config/RedisLocalConfig.java | 40 +++++ ....properties => application-dev.properties} | 77 ++++++--- .../resources/application-local.properties | 2 +- .../resources/application-prod.properties | 128 +++++++++++++++ src/main/resources/application.properties | 9 +- start.sh | 13 -- stop.sh | 7 + 94 files changed, 2518 insertions(+), 111 deletions(-) create mode 100644 app.sh create mode 100644 certs/app_certs/ca.pem create mode 100644 certs/app_certs/client-ca.pem create mode 100644 certs/app_certs/client-cert.pem create mode 100644 certs/app_certs/client-key-nopass.pem create mode 100644 certs/app_certs/client-key.pem create mode 100644 certs/app_certs/client.csr create mode 100644 certs/app_certs/google-fullchain.pem create mode 100644 certs/app_certs/google-intermediate.pem create mode 100644 certs/app_certs/google-root.pem create mode 100644 certs/app_certs/google-server.pem create mode 100644 certs/app_certs/google.crt create mode 100644 certs/app_certs/kakao.crt create mode 100644 certs/app_certs/keystore.p12 create mode 100644 certs/app_certs/keystore.pem create mode 100644 certs/app_certs/naver.crt create mode 100644 certs/app_certs/server-cert.pem create mode 100644 certs/app_certs/server-full-chain.pem create mode 100644 certs/app_certs/server-key.pem create mode 100644 certs/app_certs/server.csr create mode 100644 certs/app_certs/truststore.p12 create mode 100644 certs/app_certs/truststore.pem create mode 100644 certs/ca-key.pem create mode 100644 certs/ca.pem create mode 100644 certs/ca.srl create mode 100644 certs/intermediate-key.pem create mode 100644 certs/intermediate.csr create mode 100644 certs/intermediate.pem create mode 100644 certs/intermediate.srl create mode 100644 certs/mysql_certs/ca.pem create mode 100644 certs/mysql_certs/server-cert.pem create mode 100644 certs/mysql_certs/server-full-chain.pem create mode 100644 certs/mysql_certs/server-key-nopass.pem create mode 100644 certs/mysql_certs/server-key.pem create mode 100644 certs/mysql_certs/server.csr create mode 100644 certs/nginx_certs/ca.pem create mode 100644 certs/nginx_certs/client-ca.pem create mode 100644 certs/nginx_certs/client-cert.pem create mode 100644 certs/nginx_certs/client-key-nopass.pem create mode 100644 certs/nginx_certs/client-key.pem create mode 100644 certs/nginx_certs/client.csr create mode 100644 certs/nginx_certs/dhparam.pem create mode 100644 certs/nginx_certs/fullchain.pem create mode 100644 certs/nginx_certs/keystore.p12 create mode 100644 certs/nginx_certs/privkey.pem create mode 100644 certs/nginx_certs/server-cert.pem create mode 100644 certs/nginx_certs/server-full-chain.pem create mode 100644 certs/nginx_certs/server-key-nopass.pem create mode 100644 certs/nginx_certs/server-key.pem create mode 100644 certs/nginx_certs/server.csr create mode 100644 certs/nginx_certs/truststore.p12 create mode 100644 certs/redis_certs/ca.pem create mode 100644 certs/redis_certs/server-cert.pem create mode 100644 certs/redis_certs/server-full-chain.pem create mode 100644 certs/redis_certs/server-key.pem create mode 100644 certs/redis_certs/server.csr create mode 100644 cleanup.sh create mode 100644 deploy.sh create mode 100644 mysql.sh create mode 100644 nginx.conf create mode 100644 nginx.sh create mode 100644 ngrok.log create mode 100644 redeploy.sh create mode 100644 redis.conf create mode 100644 redis.sh create mode 100644 src/main/java/apptive/devlog/common/response/error/exception/InvalidEmailVerificationTokenException.java create mode 100644 src/main/java/apptive/devlog/documentation/controller/RootRedirectController.java create mode 100644 src/main/java/apptive/devlog/domain/auth/controller/EmailVerificationController.java create mode 100644 src/main/java/apptive/devlog/domain/auth/dto/EmailSendRequestDto.java create mode 100644 src/main/java/apptive/devlog/domain/auth/dto/EmailVerifyRequestDto.java create mode 100644 src/main/java/apptive/devlog/domain/auth/dto/EmailVerifyResponseDto.java create mode 100644 src/main/java/apptive/devlog/domain/auth/repository/VerifiedEmailRepository.java create mode 100644 src/main/java/apptive/devlog/domain/auth/service/EmailVerificationService.java create mode 100644 src/main/java/apptive/devlog/domain/auth/util/MailSender.java create mode 100644 src/main/java/apptive/devlog/domain/auth/util/RandomCodeGenerator.java create mode 100644 src/main/java/apptive/devlog/global/config/MailConfig.java delete mode 100644 src/main/java/apptive/devlog/infrastructure/redis/config/RedisConfig.java create mode 100644 src/main/java/apptive/devlog/infrastructure/redis/config/RedisJedisMutualTlsConfig.java create mode 100644 src/main/java/apptive/devlog/infrastructure/redis/config/RedisLocalConfig.java rename src/main/resources/{application-release.properties => application-dev.properties} (57%) create mode 100644 src/main/resources/application-prod.properties delete mode 100644 start.sh create mode 100644 stop.sh diff --git a/.gitignore b/.gitignore index a8f4b52..bad8fef 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ src/main/resources/application-secret.properties src/main/resources/keystore.p12 docker-compose.yml src/test/resources/application-secret.properties +init.sql +Dockerfile diff --git a/app.sh b/app.sh new file mode 100644 index 0000000..cc8dab7 --- /dev/null +++ b/app.sh @@ -0,0 +1,3 @@ +docker logs devlog-app + +docker exec -it devlog-app bash diff --git a/build.gradle b/build.gradle index 39528ee..bc61aad 100644 --- a/build.gradle +++ b/build.gradle @@ -24,35 +24,51 @@ repositories { } dependencies { + // Spring Boot Starters implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-mail' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' - implementation 'de.huxhorn.sulky:de.huxhorn.sulky.ulid:8.2.0' - implementation 'com.github.f4b6a3:ulid-creator:5.2.0' - - annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' - + // JWT (JSON Web Token) implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + // Redis Lettuce + // implementation 'io.lettuce:lettuce-core:6.2.5.RELEASE' + + implementation ('org.springframework.boot:spring-boot-starter-data-redis') { exclude group: 'io.lettuce', module: 'lettuce-core' } + implementation 'redis.clients:jedis' + // SpringDoc OpenAPI (for Swagger UI) + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + + // ULID Generators + implementation 'de.huxhorn.sulky:de.huxhorn.sulky.ulid:8.2.0' + implementation 'com.github.f4b6a3:ulid-creator:5.2.0' + + // Lombok (Compile-time annotations) compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' + // MySQL Driver (runtime only) runtimeOnly 'com.mysql:mysql-connector-j' + // Development tools (for hot-reloading) + developmentOnly 'org.springframework.boot:spring-boot-devtools' + + // Test dependencies testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // Annotation processor for Spring Boot configuration + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' } tasks.named('test') { diff --git a/certs/app_certs/ca.pem b/certs/app_certs/ca.pem new file mode 100644 index 0000000..bfcc81c --- /dev/null +++ b/certs/app_certs/ca.pem @@ -0,0 +1,40 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgUCFFw5T+2r/PXbAFYdJUeCO9NurTMhMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDE0NzIwWhcNMzUw +NDE0MDE0NzIwWjBLMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMQ0wCwYDVQQDDARXVURDMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAntyNuLTcMjWIgfrSPi+pvf3hNsHLzlV1 +Ze+hXvqs4fE0p36qVbWRUkwv+Vp4drKrlXrY5Em7JCNZ0IT9PuZ3PwFbnG5FwV+O +H3B7WrRx4JHhLZU3w+WHFGfWFDZS2eYWrf1f1tDOlyq92xx0QEqDNE+gUMJ+z1N7 +5yoDForShgTmxD+KzYp4e7BvKKKn/U6YosBC8GyiMWdcka0IUMoTwpQRQ3Vlx/KX ++YGJlwWA9wvNWOqgcYCxeCIa7X+8K9i92Py01T82zqLMcr52Ri2JEdMzHz4pp/aZ +d56pfUhGuN6ET18fTTGeKuB1PrPu80OgfOYvJL9u1/twuO/xi/naBQIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBAQAudZAxVxMh/aUeTj6WK5mpVpUINmAwt938FVSQ3nUC +mTafNjSNfu/Me+PvjCkmf9RNk5BSdHVwxseOBaU0WZnvLM056ayyQnSwWT53WwtZ +CSb3VHBbwaJqAiAnhQxCHHhWDK4rtIPrWZ/SCek2E5l29ccZHTuVnaFF3pG8YRs1 ++UXVWeCB0fIJwQdugtGkzVzgO1HW7tKyG1BqCP9q9FnfODia9d6jKAA5dScBPanG +yPNI8eq4Fpk4R6P1jM2jw9pCJ4CW4lpgGQD0KATJve+ooHJqL3Ri6xkljNymGfbP +UD8eyRxgUaKUloQY2UbafzO+cLn2uRiB4sfn/T8hybeW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIUJQjfesPiPpsGhsWZH4oTggUWVD8wDQYJKoZIhvcNAQEL +BQAwSzELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYDVQQHDAVQdXNh +bjENMAsGA1UECgwEV1VEQzENMAsGA1UEAwwEV1VEQzAeFw0yNTA0MTYwMTQzMDda +Fw0zNTA0MTQwMTQzMDdaMEsxCzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEO +MAwGA1UEBwwFUHVzYW4xDTALBgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDczXj3rCSwS+cv2FXTRWxcLxuE +m0DY9RqQfUZ6XM24IuBpxeicqIcGs8lYSWmx9cRFnxDG5beV5BSBOt3MoR9Q5Caf +Ld4JWHEIXicDk78vQjTE7QB1xPkieU0s4wqh9W0rPKEYsJ8SEhFgMUq5g5EiaVqQ +PRaaEeK1C0Zn0e5NdwcMgJ6E6uwij9E1U95QAw6AFyvqoS+s/1TsEcAkN3oIXdlD +kYy7lO4uLZI4Dfh77kfAf3LxKN/qE6WE20wyzipwb7yXFVzcTm5WoLSwV4axWrYF +Bc67x3X5t6K985Y1f5UMfnKq+JRAl0wFC0SOhS8NXHFVNQCbXvwldfcUSwc/AgMB +AAGjUzBRMB0GA1UdDgQWBBRa5Q7LNg15ZZYZSMQoowHLNtJtezAfBgNVHSMEGDAW +gBRa5Q7LNg15ZZYZSMQoowHLNtJtezAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQDXLVSeVyhCuw4oJoRw/wPwQEeBScxmpkSQvMzl19QhFXkxZxUT +5vhkCJc3lp1Ifa405n5I1+WFVhWyF1cSTXYddxz/eLjW1p54VNPXm5nxKzwwyftE +Ps36G32D0kdkk9pRqnsQ2Dt5j8hf2RVXzyswu97mTkRXTQMDwKUtwopr6HlIZiOY +BzUOEJf4NQr3VeGkXeWfqjWMU7ch7FWf7SsyqNFinAgiDLaIdEYSRBi8xiRJpOn4 +BUbhAlS2L4uGPWGa7TZ8sf8/s4Mdh784NSFn+4D6pVa7Quq5e6OmBVK4J851zAic +7mQsPhJLXactGWdksrjBSMQS7E5R8LG+k0j3 +-----END CERTIFICATE----- diff --git a/certs/app_certs/client-ca.pem b/certs/app_certs/client-ca.pem new file mode 100644 index 0000000..bfcc81c --- /dev/null +++ b/certs/app_certs/client-ca.pem @@ -0,0 +1,40 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgUCFFw5T+2r/PXbAFYdJUeCO9NurTMhMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDE0NzIwWhcNMzUw +NDE0MDE0NzIwWjBLMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMQ0wCwYDVQQDDARXVURDMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAntyNuLTcMjWIgfrSPi+pvf3hNsHLzlV1 +Ze+hXvqs4fE0p36qVbWRUkwv+Vp4drKrlXrY5Em7JCNZ0IT9PuZ3PwFbnG5FwV+O +H3B7WrRx4JHhLZU3w+WHFGfWFDZS2eYWrf1f1tDOlyq92xx0QEqDNE+gUMJ+z1N7 +5yoDForShgTmxD+KzYp4e7BvKKKn/U6YosBC8GyiMWdcka0IUMoTwpQRQ3Vlx/KX ++YGJlwWA9wvNWOqgcYCxeCIa7X+8K9i92Py01T82zqLMcr52Ri2JEdMzHz4pp/aZ +d56pfUhGuN6ET18fTTGeKuB1PrPu80OgfOYvJL9u1/twuO/xi/naBQIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBAQAudZAxVxMh/aUeTj6WK5mpVpUINmAwt938FVSQ3nUC +mTafNjSNfu/Me+PvjCkmf9RNk5BSdHVwxseOBaU0WZnvLM056ayyQnSwWT53WwtZ +CSb3VHBbwaJqAiAnhQxCHHhWDK4rtIPrWZ/SCek2E5l29ccZHTuVnaFF3pG8YRs1 ++UXVWeCB0fIJwQdugtGkzVzgO1HW7tKyG1BqCP9q9FnfODia9d6jKAA5dScBPanG +yPNI8eq4Fpk4R6P1jM2jw9pCJ4CW4lpgGQD0KATJve+ooHJqL3Ri6xkljNymGfbP +UD8eyRxgUaKUloQY2UbafzO+cLn2uRiB4sfn/T8hybeW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIUJQjfesPiPpsGhsWZH4oTggUWVD8wDQYJKoZIhvcNAQEL +BQAwSzELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYDVQQHDAVQdXNh +bjENMAsGA1UECgwEV1VEQzENMAsGA1UEAwwEV1VEQzAeFw0yNTA0MTYwMTQzMDda +Fw0zNTA0MTQwMTQzMDdaMEsxCzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEO +MAwGA1UEBwwFUHVzYW4xDTALBgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDczXj3rCSwS+cv2FXTRWxcLxuE +m0DY9RqQfUZ6XM24IuBpxeicqIcGs8lYSWmx9cRFnxDG5beV5BSBOt3MoR9Q5Caf +Ld4JWHEIXicDk78vQjTE7QB1xPkieU0s4wqh9W0rPKEYsJ8SEhFgMUq5g5EiaVqQ +PRaaEeK1C0Zn0e5NdwcMgJ6E6uwij9E1U95QAw6AFyvqoS+s/1TsEcAkN3oIXdlD +kYy7lO4uLZI4Dfh77kfAf3LxKN/qE6WE20wyzipwb7yXFVzcTm5WoLSwV4axWrYF +Bc67x3X5t6K985Y1f5UMfnKq+JRAl0wFC0SOhS8NXHFVNQCbXvwldfcUSwc/AgMB +AAGjUzBRMB0GA1UdDgQWBBRa5Q7LNg15ZZYZSMQoowHLNtJtezAfBgNVHSMEGDAW +gBRa5Q7LNg15ZZYZSMQoowHLNtJtezAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQDXLVSeVyhCuw4oJoRw/wPwQEeBScxmpkSQvMzl19QhFXkxZxUT +5vhkCJc3lp1Ifa405n5I1+WFVhWyF1cSTXYddxz/eLjW1p54VNPXm5nxKzwwyftE +Ps36G32D0kdkk9pRqnsQ2Dt5j8hf2RVXzyswu97mTkRXTQMDwKUtwopr6HlIZiOY +BzUOEJf4NQr3VeGkXeWfqjWMU7ch7FWf7SsyqNFinAgiDLaIdEYSRBi8xiRJpOn4 +BUbhAlS2L4uGPWGa7TZ8sf8/s4Mdh784NSFn+4D6pVa7Quq5e6OmBVK4J851zAic +7mQsPhJLXactGWdksrjBSMQS7E5R8LG+k0j3 +-----END CERTIFICATE----- diff --git a/certs/app_certs/client-cert.pem b/certs/app_certs/client-cert.pem new file mode 100644 index 0000000..0efca3f --- /dev/null +++ b/certs/app_certs/client-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJjCCAg4CFFw5T+2r/PXbAFYdJUeCO9NurTMjMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDU1OTAyWhcNMjYw +NDE2MDU1OTAyWjBUMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMRYwFAYDVQQDDA1kZXZsb2ctY2xpZW50 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FizfydyA4/N8rtlV4D3 +YNkeWNs1afvPP5ylut7LgVXHqQvp3UuCMlmG4fHkdH7QAm5bxTgQnAWRCYFQ3/M2 +/eizc44LphuKkHZ8+uNKOZpjNjFXLzPe+m67hd+qVJJq0afscp+/UQkF348mxmoR +92nfaf1YWSWmdEU15iVahWAXyN8QUWfd6LanwXS2hd9u3ZzwIwGpYmf0JjnbLpvl +FrDkBEX3n56xe1SBoYnBC8tz9Ljelg3WHfTBaAj23wxEuJpTrfKnc4CGT+A0KFoi +12uRyNN0Qe1VroCyFKN75DjRc7KvZd5WjXf6WTXo83GoAVH89hGCW0EEyRgfpteK +7wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQALrx8OeaBJOvH/wTIA9UxCECd3pLfF +ojnitMDHs45RDzrO8adxqTGuU1yyHtCpVtJOI7OLN+De3uVgSWb5fU9SRO9tgE4m +0WAgQXkxDVXrV2AThUnzyDNkWynxad4ledhVJ8hJN4OAMGA8dg9jovP3OMVjScZP +nQ+JIqhai1Ony6k7BTPwSXoByOouRG5M1c6HsKW0JL+mziz5qKh6UQE1O2Wu2BBR +aoFnAogK5WWBhYxntdX5WfIfGz2Pq/NjDyXwbSnLliOmr1HoXLpDtOoPTmnjbqD3 +m2ytsApd7YRYiM88w7VRtt5VzYXyrqPkWCNBn9xyeNKnQwprrRxfMKj1 +-----END CERTIFICATE----- diff --git a/certs/app_certs/client-key-nopass.pem b/certs/app_certs/client-key-nopass.pem new file mode 100644 index 0000000..65df333 --- /dev/null +++ b/certs/app_certs/client-key-nopass.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDcWLN/J3IDj83y +u2VXgPdg2R5Y2zVp+88/nKW63suBVcepC+ndS4IyWYbh8eR0ftACblvFOBCcBZEJ +gVDf8zb96LNzjgumG4qQdnz640o5mmM2MVcvM976bruF36pUkmrRp+xyn79RCQXf +jybGahH3ad9p/VhZJaZ0RTXmJVqFYBfI3xBRZ93otqfBdLaF327dnPAjAaliZ/Qm +Odsum+UWsOQERfefnrF7VIGhicELy3P0uN6WDdYd9MFoCPbfDES4mlOt8qdzgIZP +4DQoWiLXa5HI03RB7VWugLIUo3vkONFzsq9l3laNd/pZNejzcagBUfz2EYJbQQTJ +GB+m14rvAgMBAAECggEAAXr+7K6PKkhEaXqk7aAcceAtXqH++6xfZ5w2pMPB9WDo +xwBatvWsYuvZWltHLIcUH67AbCgv3i15HHv49nss/1R3nZTbJgmUw0idasMYly9i +2u8XpblhC90NDFm1LQ+OIKp8aW9GZWLdPEyZG89vNF3IGtKbtn7tsSxBTLKOp1hg +IhMTK0VToA5lCbGn0JPSJlZTxSlKpz1HK/WxVWnGWGeSvIdBt33o0Ea9IHTT1esg +j7zr8jVz9FWcteiyIy1eRptKa+zLTtwLdRL0C1l20Sg2flml/k4UzwuexpOlg7VL +qsp7drzOfq/wPew88W3edhbpe52agsS29yoOtD6XgQKBgQDv8RAZnNAKWvcrqYAe +M9NBpMp/7+zeoK5u3A7LwUr9AOTYH9WdD53Y+3pn+ER7Dakn1o8TEDhwC0K623yX +uwHjMGeDHMifDGIe+FIVvJVVYf0I5qMGojUUbDDswQf3e5axrV1jJnk5++QaYHba +L9Ap+FweQfkmoPGiq3h74atIqQKBgQDrF+m4Wvwib27g2O285T5SmY2hPQwqysg1 +N30IBV/m6brwKf7mcXdp1UF1od6jIG8FutI5kPrqfeYZShfgo20biv1YqgPAUoSF +EOjILQ0bMXadv0VZJfDqXnGAFh1c0otTpVUsZ5Qv/UYX46HdVovRuzqmp59JAIba +TsRhFEt91wKBgQCEd9xhp5eb//iyHFRlWEtr1GUQGQ/3IVLsVYW9rCuQXuv4/ipb +GgIVh1FfEUwNe89F9UjsR2pBQZZHv2GcC1zRZyne0wdX9+g8HPCEm6b+iqi+P0cG +JIuViN3B+BhD4/Ggiowib11CS/T1MwirEPamFT4WXmoFj5mYK37LNh3wcQKBgQDj +2dNeKGDChznxlo4kTBLxP33zThWiy9LrMRJvWbYvOU1DQ5CXjFVuL5A4EGCVvfOc +nArwXEG0T71ZuWQXBo3S6gzNiEoGdnOV/GOAz5kqR/Bsx1rRImKy5EIhIE3pDu6W +bWF1nhYTxOfQc4EH4r+00D/yEffhay9IGptec6sPFQKBgGY+RuvEja4LmRmxqFqf +0jybFry3ct+iM30Bp+ythjT15S9JFUvtGYuRH4NwFP3PzBKrmWPpkSdewFxPhHon +ZpSL9zWjH7W5l9HUBbuiYM5PF5nbz8uzZXQuL2zHl4dp1PYSdgHnZO4q6/oVt2iv +Y56fzbNtDQOSzNcS2KrKNBNx +-----END PRIVATE KEY----- diff --git a/certs/app_certs/client-key.pem b/certs/app_certs/client-key.pem new file mode 100644 index 0000000..949ac8d --- /dev/null +++ b/certs/app_certs/client-key.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQID1XMsFyUjIQCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBxxVDXKG4mZOsVmGBYEITbBIIE +0HwgneRxrrWTxCUNrmpOcDZ7Sl9Xr1DhZYOwa7W5aqwzDhToxB2Q4PMTzPQJtJYx +wzUgP5bEK8t4tIKoEulu9yXbMMhlYWhHBR0dB7VReyvLispZdduOHeJ5kf1mCwPa +l6q6ZdGkjG7+P9CGx3Sfv995vXMtbuDjgtpoXqQ1A7frplmfMjh0BUcG22UwLbUB +XhjJ35iYgXIbeR54F09Pg0bg8VkYcS05+DsSdWS4r3IYDVauBZg+cXCb0aRl+GXO +jnmYYDymkJEzi1Fw4f7wg7X8QKuyUadYGxhnK0q0znSkOMXB7SVISWYajWsUCB+p +yBWU7KdafsntAqaDNmpph24PJEWGyimPmVjU7KgHxqyrYb/taXrsiN22sFEPX3Ta +5ye1QUCu5p6EFPxoKZt0WU4/75fwAsGypmxpHeWgf3qQmewpVbQFzkqmbs1sD6NI +1Q9dMLoG5CopS7MhOsfRRITEhM9es4CjlTo1EJZsBkS5ZBsXoiMhTRqmazDA+bhd +R+cPQRxQ0S62hRX85BQAAy6ONxbLyHiEo4XVhVOD/OdoJxmFpVQCVteErqTDSWJa +PHkx7ZjAOgUR2r1/EX6pVIky1omFyiRD4AGX/dguDIEJxTwCTiayNRvedUSxIfg+ +XGJWfs7Nj7V9I8WF9sGRuBYkOS8GAjaYJpU5Fw1CThFU0VtlcpF8WioQhoTR3Soi +dqLuq6TyYEeADLRNL37YxSI1GcbK4Wf3RjJ6uoGyosFS8JLSlPE2Ib7Rrj6LL8sm +1KNUY+Kgmn/5mirLRTM+MxRXUXdk6wzOknLdNkeQwrWJHCJm0qZtyjRPPK2vwNta +p2HkvyxmlTqQx3b3+OcAis+Bjf2H/jzH1kAUV4MWgT3bHhUaQWcS/baBC9QiBaMc +qM6ejP9/GZaljkUMOupAoOz8pnY5ewVMbSl5+IPZBa7K24wgKiHZFzjMVCGzDmzr +WgiTbOubOOb9OaTR/iDZG+3sAtkPi5WsgVHrYyV01SUwS4uetxdJJA6T0nsw2SjN +PJ3WkSMRoUiTt5Ij6XsH2pzuc/osxEAtm0xNflW5FSPtg/ADK2wd9MT0PZCmo+ba +OYvMoO9aJS25PP3XK+nXHaQh7vxeroP/KGEtYWWCiRvZeZ0cX+3jhevHHQJ4mjbn +MIbD0/qsVBpFksqPKWlVKjjaFVr/yTLqSb69zVM15Ah3al7sKiUbgqbGLqM92zIt +jeyGNw5o8tRhmsCeUEvpXP4GVGBanio30ikqMb1z5OrG6XB7fy/JjSLksCRBB9uV +Vq8HCe4OdKDO/vDN8mp2DxZL+OGd/hEqdUZ0CyKrmBO3lU3h4yLNh2QFdNXimCjK +6nyDu6vhdAF6j2iUWC/sOOuEt7QELYGBCdBbn0wfH/XaWneDZ0IYwvSdbTEw9qoQ +wvboCFX49g3pkbIj10yp5gDo6mE9bPzPyjeud/Ag8y3LlS/MO+uhKv5XupD2+3sl +3iwTgSy+iQjHDII0AVb+QaavVthHkRHNL/rl0n/fmyZHzFDALNXIJ/yNPxL85D78 +MiMYQJ9E+rO+ZYWwmRXW+FxfZ1JyaDQsPV1ZWqJdmpiA3vnL3ZL4xrXK25w6tZy3 +FlfN+bnP1r2LNQFKBrERky0RBCU8iv6xQUbkQyPFThh0 +-----END ENCRYPTED PRIVATE KEY----- diff --git a/certs/app_certs/client.csr b/certs/app_certs/client.csr new file mode 100644 index 0000000..de9ebc0 --- /dev/null +++ b/certs/app_certs/client.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICmTCCAYECAQAwVDELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYD +VQQHDAVQdXNhbjENMAsGA1UECgwEV1VEQzEWMBQGA1UEAwwNZGV2bG9nLWNsaWVu +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxYs38ncgOPzfK7ZVeA +92DZHljbNWn7zz+cpbrey4FVx6kL6d1LgjJZhuHx5HR+0AJuW8U4EJwFkQmBUN/z +Nv3os3OOC6YbipB2fPrjSjmaYzYxVy8z3vpuu4XfqlSSatGn7HKfv1EJBd+PJsZq +Efdp32n9WFklpnRFNeYlWoVgF8jfEFFn3ei2p8F0toXfbt2c8CMBqWJn9CY52y6b +5Raw5ARF95+esXtUgaGJwQvLc/S43pYN1h30wWgI9t8MRLiaU63yp3OAhk/gNCha +ItdrkcjTdEHtVa6AshSje+Q40XOyr2XeVo13+lk16PNxqAFR/PYRgltBBMkYH6bX +iu8CAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQC4Vzu4fyhewiMXIFYptfr5kaDr +4BFKBqDQHZERgSoKlM8bIA5+TCTVGQ38mUGiDbuu+34XFseJC9dP9m8OED6Hmgm9 +yCWN+64GFiC1jOlkFw2VmE6rHH6N2sbwOgTPy91YJh8kPnjr92i/2qHXX89T7LcW +IvQE6e0TlIXan43udpXKH97z8DVUdfwakd3gH0K2wsAmUtSfYjeZty+EdB/LYpFw +jvDnw7jbpHDtkWAf/w9lpgibrYzC7FNl9JaHq6ng24J3ude4kAHQ4QKU8Z8jkiJn +6Koe4topx8xbkBieAdNzLMgi0nv+tY4/UMMI1njk7gVbsyQ7wY5Ir6qvm4hZ +-----END CERTIFICATE REQUEST----- diff --git a/certs/app_certs/google-fullchain.pem b/certs/app_certs/google-fullchain.pem new file mode 100644 index 0000000..8ac0960 --- /dev/null +++ b/certs/app_certs/google-fullchain.pem @@ -0,0 +1,89 @@ +-----BEGIN CERTIFICATE----- +MIIEeDCCA2CgAwIBAgIRAIIOTeFcOkvUEAhZaRElMkYwDQYJKoZIhvcNAQELBQAw +OzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFUdvb2dsZSBUcnVzdCBTZXJ2aWNlczEM +MAoGA1UEAxMDV1IyMB4XDTI1MDMyMDExMjAzNFoXDTI1MDYxMjExMjAzM1owHjEc +MBoGA1UEAxMTYWNjb3VudHMuZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABLxsHfzEUtktVB6CycZH6AaINi5Bhsw07rZEuJhOE3X/gATjzeZNf+1j +pl/fApfNdgTlNWCFrrP36YTzx+82aOOjggJdMIICWTAOBgNVHQ8BAf8EBAMCB4Aw +EwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUliDn +8rqSai7lTNzAcR/TSF65DKQwHwYDVR0jBBgwFoAU3hse7XkV1D43JMMhu+w0OW1C +sjAwWAYIKwYBBQUHAQEETDBKMCEGCCsGAQUFBzABhhVodHRwOi8vby5wa2kuZ29v +Zy93cjIwJQYIKwYBBQUHMAKGGWh0dHA6Ly9pLnBraS5nb29nL3dyMi5jcnQwNQYD +VR0RBC4wLIITYWNjb3VudHMuZ29vZ2xlLmNvbYIVKi5wYXJ0bmVyLmFuZHJvaWQu +Y29tMBMGA1UdIAQMMAowCAYGZ4EMAQIBMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6 +Ly9jLnBraS5nb29nL3dyMi83NXI0WnlBM3ZBMC5jcmwwggEEBgorBgEEAdZ5AgQC +BIH1BIHyAPAAdwDPEVbu1S58r/OHW9lpLpvpGnFnSrAX7KwB0lt3zsw7CAAAAZWz +f4ySAAAEAwBIMEYCIQCynwunRTr3Zl0HlSl791D5pedOwAhFDr9T750YJAGPzAIh +AP3zIGEjXeps4Ft1FUwfl3Yco+L1fkyybt4YT77pfBZ+AHUAouMK5EXvva2bfjjt +R2d3U9eCW4SU1yteGyzEuVCkR+cAAAGVs3+MdQAABAMARjBEAiA+h5fkWNb51WXn +kDYKXu+FW43PCjAhzfskOo5d6W6mQwIgT3ky97SykxawRlhso0Z75lCh/2qqLdPb +Ro8LyQDOxLswDQYJKoZIhvcNAQELBQADggEBAHFdiq+W1/SaIFfURWnomlywdd31 +1o8b/YH/K9tUbzB90FrtXDc8Y9+GbZ6naNAYeoQVUrRmlaZuPlTJbpjVDLoJu+1i +WrzpT6On00Z8RJJmBV/gtYJ6s3GxMpFlDmXZDS0W2yNvs9O2YoRQdjNYJ9lnjQEv +NgoI9oOJ5s37xqtijV7qyALHzD2+dZlP77jzaa+18U0j1cV3V4vmBED/cNxlaX1i +bAO53XukfH5ylvOxB5K09mIA2OYYU5N8U9XBFGRVBKLfzo0rdmnt9XqvZsH1CyKJ +tUS0x6P17D4f3mOEqVjQMJG7EVy4smqEUnRfOIrleHIFXMfv8o6MPVlgc9I= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFCzCCAvOgAwIBAgIQf/AFoHxM3tEArZ1mpRB7mDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM +QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIw +MTQwMDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNl +cnZpY2VzMQwwCgYDVQQDEwNXUjIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCp/5x/RR5wqFOfytnlDd5GV1d9vI+aWqxG8YSau5HbyfsvAfuSCQAWXqAc ++MGr+XgvSszYhaLYWTwO0xj7sfUkDSbutltkdnwUxy96zqhMt/TZCPzfhyM1IKji +aeKMTj+xWfpgoh6zySBTGYLKNlNtYE3pAJH8do1cCA8Kwtzxc2vFE24KT3rC8gIc +LrRjg9ox9i11MLL7q8Ju26nADrn5Z9TDJVd06wW06Y613ijNzHoU5HEDy01hLmFX +xRmpC5iEGuh5KdmyjS//V2pm4M6rlagplmNwEmceOuHbsCFx13ye/aoXbv4r+zgX +FNFmp6+atXDMyGOBOozAKql2N87jAgMBAAGjgf4wgfswDgYDVR0PAQH/BAQDAgGG +MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/ +AgEAMB0GA1UdDgQWBBTeGx7teRXUPjckwyG77DQ5bUKyMDAfBgNVHSMEGDAWgBTk +rysmcRorSCeFL1JmLO/wiRNxPjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAKG +GGh0dHA6Ly9pLnBraS5nb29nL3IxLmNydDArBgNVHR8EJDAiMCCgHqAchhpodHRw +Oi8vYy5wa2kuZ29vZy9yL3IxLmNybDATBgNVHSAEDDAKMAgGBmeBDAECATANBgkq +hkiG9w0BAQsFAAOCAgEARXWL5R87RBOWGqtY8TXJbz3S0DNKhjO6V1FP7sQ02hYS +TL8Tnw3UVOlIecAwPJQl8hr0ujKUtjNyC4XuCRElNJThb0Lbgpt7fyqaqf9/qdLe +SiDLs/sDA7j4BwXaWZIvGEaYzq9yviQmsR4ATb0IrZNBRAq7x9UBhb+TV+PfdBJT +DhEl05vc3ssnbrPCuTNiOcLgNeFbpwkuGcuRKnZc8d/KI4RApW//mkHgte8y0YWu +ryUJ8GLFbsLIbjL9uNrizkqRSvOFVU6xddZIMy9vhNkSXJ/UcZhjJY1pXAprffJB +vei7j+Qi151lRehMCofa6WBmiA4fx+FOVsV2/7R6V2nyAiIJJkEd2nSi5SnzxJrl +Xdaqev3htytmOPvoKWa676ATL/hzfvDaQBEcXd2Ppvy+275W+DKcH0FBbX62xevG +iza3F4ydzxl6NJ8hk8R+dDXSqv1MbRT1ybB5W0k8878XSOjvmiYTDIfyc9acxVJr +Y/cykHipa+te1pOhv7wYPYtZ9orGBV5SGOJm4NrB3K1aJar0RfzxC3ikr7Dyc6Qw +qDTBU39CluVIQeuQRgwG3MuSxl7zRERDRilGoKb8uY45JzmxWuKxrfwT/478JuHU +/oTxUFqOl2stKnn7QGTq8z29W+GgBLCXSBxC9epaHM0myFH/FJlniXJfHeytWt0= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBX +MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE +CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYx +OTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT +GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63 +ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwS +iV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351k +KSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZ +DrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zk +j5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5 +cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esW +CruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499 +iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35Ei +Eua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbap +sZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b +9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAf +BgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIw +JQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUH +MAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6Al +oCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAy +MAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIF +AwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9 +NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9 +WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw +9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy ++qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvi +d0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8= +-----END CERTIFICATE----- + diff --git a/certs/app_certs/google-intermediate.pem b/certs/app_certs/google-intermediate.pem new file mode 100644 index 0000000..82eee6d --- /dev/null +++ b/certs/app_certs/google-intermediate.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFCzCCAvOgAwIBAgIQf/AFoHxM3tEArZ1mpRB7mDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM +QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIw +MTQwMDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNl +cnZpY2VzMQwwCgYDVQQDEwNXUjIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCp/5x/RR5wqFOfytnlDd5GV1d9vI+aWqxG8YSau5HbyfsvAfuSCQAWXqAc ++MGr+XgvSszYhaLYWTwO0xj7sfUkDSbutltkdnwUxy96zqhMt/TZCPzfhyM1IKji +aeKMTj+xWfpgoh6zySBTGYLKNlNtYE3pAJH8do1cCA8Kwtzxc2vFE24KT3rC8gIc +LrRjg9ox9i11MLL7q8Ju26nADrn5Z9TDJVd06wW06Y613ijNzHoU5HEDy01hLmFX +xRmpC5iEGuh5KdmyjS//V2pm4M6rlagplmNwEmceOuHbsCFx13ye/aoXbv4r+zgX +FNFmp6+atXDMyGOBOozAKql2N87jAgMBAAGjgf4wgfswDgYDVR0PAQH/BAQDAgGG +MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/ +AgEAMB0GA1UdDgQWBBTeGx7teRXUPjckwyG77DQ5bUKyMDAfBgNVHSMEGDAWgBTk +rysmcRorSCeFL1JmLO/wiRNxPjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAKG +GGh0dHA6Ly9pLnBraS5nb29nL3IxLmNydDArBgNVHR8EJDAiMCCgHqAchhpodHRw +Oi8vYy5wa2kuZ29vZy9yL3IxLmNybDATBgNVHSAEDDAKMAgGBmeBDAECATANBgkq +hkiG9w0BAQsFAAOCAgEARXWL5R87RBOWGqtY8TXJbz3S0DNKhjO6V1FP7sQ02hYS +TL8Tnw3UVOlIecAwPJQl8hr0ujKUtjNyC4XuCRElNJThb0Lbgpt7fyqaqf9/qdLe +SiDLs/sDA7j4BwXaWZIvGEaYzq9yviQmsR4ATb0IrZNBRAq7x9UBhb+TV+PfdBJT +DhEl05vc3ssnbrPCuTNiOcLgNeFbpwkuGcuRKnZc8d/KI4RApW//mkHgte8y0YWu +ryUJ8GLFbsLIbjL9uNrizkqRSvOFVU6xddZIMy9vhNkSXJ/UcZhjJY1pXAprffJB +vei7j+Qi151lRehMCofa6WBmiA4fx+FOVsV2/7R6V2nyAiIJJkEd2nSi5SnzxJrl +Xdaqev3htytmOPvoKWa676ATL/hzfvDaQBEcXd2Ppvy+275W+DKcH0FBbX62xevG +iza3F4ydzxl6NJ8hk8R+dDXSqv1MbRT1ybB5W0k8878XSOjvmiYTDIfyc9acxVJr +Y/cykHipa+te1pOhv7wYPYtZ9orGBV5SGOJm4NrB3K1aJar0RfzxC3ikr7Dyc6Qw +qDTBU39CluVIQeuQRgwG3MuSxl7zRERDRilGoKb8uY45JzmxWuKxrfwT/478JuHU +/oTxUFqOl2stKnn7QGTq8z29W+GgBLCXSBxC9epaHM0myFH/FJlniXJfHeytWt0= +-----END CERTIFICATE----- + diff --git a/certs/app_certs/google-root.pem b/certs/app_certs/google-root.pem new file mode 100644 index 0000000..455abc9 --- /dev/null +++ b/certs/app_certs/google-root.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBX +MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE +CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYx +OTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT +GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63 +ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwS +iV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351k +KSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZ +DrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zk +j5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5 +cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esW +CruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499 +iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35Ei +Eua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbap +sZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b +9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAf +BgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIw +JQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUH +MAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6Al +oCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAy +MAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIF +AwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9 +NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9 +WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw +9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy ++qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvi +d0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8= +-----END CERTIFICATE----- + diff --git a/certs/app_certs/google-server.pem b/certs/app_certs/google-server.pem new file mode 100644 index 0000000..9ef5df6 --- /dev/null +++ b/certs/app_certs/google-server.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEeDCCA2CgAwIBAgIRAIIOTeFcOkvUEAhZaRElMkYwDQYJKoZIhvcNAQELBQAw +OzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFUdvb2dsZSBUcnVzdCBTZXJ2aWNlczEM +MAoGA1UEAxMDV1IyMB4XDTI1MDMyMDExMjAzNFoXDTI1MDYxMjExMjAzM1owHjEc +MBoGA1UEAxMTYWNjb3VudHMuZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABLxsHfzEUtktVB6CycZH6AaINi5Bhsw07rZEuJhOE3X/gATjzeZNf+1j +pl/fApfNdgTlNWCFrrP36YTzx+82aOOjggJdMIICWTAOBgNVHQ8BAf8EBAMCB4Aw +EwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUliDn +8rqSai7lTNzAcR/TSF65DKQwHwYDVR0jBBgwFoAU3hse7XkV1D43JMMhu+w0OW1C +sjAwWAYIKwYBBQUHAQEETDBKMCEGCCsGAQUFBzABhhVodHRwOi8vby5wa2kuZ29v +Zy93cjIwJQYIKwYBBQUHMAKGGWh0dHA6Ly9pLnBraS5nb29nL3dyMi5jcnQwNQYD +VR0RBC4wLIITYWNjb3VudHMuZ29vZ2xlLmNvbYIVKi5wYXJ0bmVyLmFuZHJvaWQu +Y29tMBMGA1UdIAQMMAowCAYGZ4EMAQIBMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6 +Ly9jLnBraS5nb29nL3dyMi83NXI0WnlBM3ZBMC5jcmwwggEEBgorBgEEAdZ5AgQC +BIH1BIHyAPAAdwDPEVbu1S58r/OHW9lpLpvpGnFnSrAX7KwB0lt3zsw7CAAAAZWz +f4ySAAAEAwBIMEYCIQCynwunRTr3Zl0HlSl791D5pedOwAhFDr9T750YJAGPzAIh +AP3zIGEjXeps4Ft1FUwfl3Yco+L1fkyybt4YT77pfBZ+AHUAouMK5EXvva2bfjjt +R2d3U9eCW4SU1yteGyzEuVCkR+cAAAGVs3+MdQAABAMARjBEAiA+h5fkWNb51WXn +kDYKXu+FW43PCjAhzfskOo5d6W6mQwIgT3ky97SykxawRlhso0Z75lCh/2qqLdPb +Ro8LyQDOxLswDQYJKoZIhvcNAQELBQADggEBAHFdiq+W1/SaIFfURWnomlywdd31 +1o8b/YH/K9tUbzB90FrtXDc8Y9+GbZ6naNAYeoQVUrRmlaZuPlTJbpjVDLoJu+1i +WrzpT6On00Z8RJJmBV/gtYJ6s3GxMpFlDmXZDS0W2yNvs9O2YoRQdjNYJ9lnjQEv +NgoI9oOJ5s37xqtijV7qyALHzD2+dZlP77jzaa+18U0j1cV3V4vmBED/cNxlaX1i +bAO53XukfH5ylvOxB5K09mIA2OYYU5N8U9XBFGRVBKLfzo0rdmnt9XqvZsH1CyKJ +tUS0x6P17D4f3mOEqVjQMJG7EVy4smqEUnRfOIrleHIFXMfv8o6MPVlgc9I= +-----END CERTIFICATE----- + diff --git a/certs/app_certs/google.crt b/certs/app_certs/google.crt new file mode 100644 index 0000000..0b18e24 --- /dev/null +++ b/certs/app_certs/google.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEeDCCA2CgAwIBAgIRAIIOTeFcOkvUEAhZaRElMkYwDQYJKoZIhvcNAQELBQAw +OzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFUdvb2dsZSBUcnVzdCBTZXJ2aWNlczEM +MAoGA1UEAxMDV1IyMB4XDTI1MDMyMDExMjAzNFoXDTI1MDYxMjExMjAzM1owHjEc +MBoGA1UEAxMTYWNjb3VudHMuZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABLxsHfzEUtktVB6CycZH6AaINi5Bhsw07rZEuJhOE3X/gATjzeZNf+1j +pl/fApfNdgTlNWCFrrP36YTzx+82aOOjggJdMIICWTAOBgNVHQ8BAf8EBAMCB4Aw +EwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUliDn +8rqSai7lTNzAcR/TSF65DKQwHwYDVR0jBBgwFoAU3hse7XkV1D43JMMhu+w0OW1C +sjAwWAYIKwYBBQUHAQEETDBKMCEGCCsGAQUFBzABhhVodHRwOi8vby5wa2kuZ29v +Zy93cjIwJQYIKwYBBQUHMAKGGWh0dHA6Ly9pLnBraS5nb29nL3dyMi5jcnQwNQYD +VR0RBC4wLIITYWNjb3VudHMuZ29vZ2xlLmNvbYIVKi5wYXJ0bmVyLmFuZHJvaWQu +Y29tMBMGA1UdIAQMMAowCAYGZ4EMAQIBMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6 +Ly9jLnBraS5nb29nL3dyMi83NXI0WnlBM3ZBMC5jcmwwggEEBgorBgEEAdZ5AgQC +BIH1BIHyAPAAdwDPEVbu1S58r/OHW9lpLpvpGnFnSrAX7KwB0lt3zsw7CAAAAZWz +f4ySAAAEAwBIMEYCIQCynwunRTr3Zl0HlSl791D5pedOwAhFDr9T750YJAGPzAIh +AP3zIGEjXeps4Ft1FUwfl3Yco+L1fkyybt4YT77pfBZ+AHUAouMK5EXvva2bfjjt +R2d3U9eCW4SU1yteGyzEuVCkR+cAAAGVs3+MdQAABAMARjBEAiA+h5fkWNb51WXn +kDYKXu+FW43PCjAhzfskOo5d6W6mQwIgT3ky97SykxawRlhso0Z75lCh/2qqLdPb +Ro8LyQDOxLswDQYJKoZIhvcNAQELBQADggEBAHFdiq+W1/SaIFfURWnomlywdd31 +1o8b/YH/K9tUbzB90FrtXDc8Y9+GbZ6naNAYeoQVUrRmlaZuPlTJbpjVDLoJu+1i +WrzpT6On00Z8RJJmBV/gtYJ6s3GxMpFlDmXZDS0W2yNvs9O2YoRQdjNYJ9lnjQEv +NgoI9oOJ5s37xqtijV7qyALHzD2+dZlP77jzaa+18U0j1cV3V4vmBED/cNxlaX1i +bAO53XukfH5ylvOxB5K09mIA2OYYU5N8U9XBFGRVBKLfzo0rdmnt9XqvZsH1CyKJ +tUS0x6P17D4f3mOEqVjQMJG7EVy4smqEUnRfOIrleHIFXMfv8o6MPVlgc9I= +-----END CERTIFICATE----- diff --git a/certs/app_certs/kakao.crt b/certs/app_certs/kakao.crt new file mode 100644 index 0000000..fcf3943 --- /dev/null +++ b/certs/app_certs/kakao.crt @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/certs/app_certs/keystore.p12 b/certs/app_certs/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..49a7bb0c40032dc09e763b87395be99a268cd59e GIT binary patch literal 3480 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 literal 0 HcmV?d00001 diff --git a/certs/app_certs/keystore.pem b/certs/app_certs/keystore.pem new file mode 100644 index 0000000..8b403af --- /dev/null +++ b/certs/app_certs/keystore.pem @@ -0,0 +1,78 @@ +Bag Attributes + friendlyName: devlog-app + localKeyID: 7A 75 ED 96 03 AC E0 78 BB DB F3 B9 DD 89 90 6D 4C 76 C7 8C +subject=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = devlog-app +issuer=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = WUDC +-----BEGIN CERTIFICATE----- +MIIDIzCCAgsCFFhWSQG3H1J81Xt7M+sUWjv4RQLAMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDIzODQ4WhcNMjYw +NDE2MDIzODQ4WjBRMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMRMwEQYDVQQDDApkZXZsb2ctYXBwMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAksMadi9QiZ6rkVmfeqGRjJ2D +NzkAROiCe+FiziwN+q82Vl8LVlt8z3DCGilMOkCwNDp8QSlp153QMy0l6yVzmxKD +X5QakyNHlQJGO7Afm7dbCMzIIxbY5LrLtGzbO9Hrc+hupVPnV/nf4/xPuOcqxwBT +T/cOSaaya5KHajXkBjDXRPNrPj50tDf0gQP+f1YiCd/wVaTv8Xuu6yLa7A5dnAaH +WalsAmhIxVHegt0bAvJ7CzMqFcSarGJEBOEjKMHXdX2s56Nr56SGsxAbkOroyItW +GgrvlfxOkkV7Nhg6+8vJc11BPW6yZ6s+6mkKvB4DKjdaYXSsGsec/98c6CYFAQID +AQABMA0GCSqGSIb3DQEBCwUAA4IBAQCNlBmDRL3e1ryjlOEgQSa49YCjDHGSrWli +PKYvdL4pJa9XXJFXssSvKRXfGlSfva+VVGkouqSbYLdwNEF1ixp3G6RGdwq14yZw ++I6bi/76Yops/JTRsekLcV3E/oqKuaIMrdo8RgB6kRmxmMNnB1+Zb9amNEc+mduM +1L7r8nPyUWsxLAUjCoW/W5KMU1JjVxm7PRJmnrBvRzA8eXnxXJBesZMJ2fUAMJCt +RvGTRy5f/M1Xb8iJw6O2zu3FAszn+eZBHsXsy1z45vNNoWH+DTFf+kCuTJqiPwNA +/4tYRkZIUbVg6NlrCSqFyxRYRY3R/hujpekvtE3W9cFBCsiDHLbp +-----END CERTIFICATE----- +Bag Attributes: +subject=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = WUDC +issuer=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = WUDC +-----BEGIN CERTIFICATE----- +MIIDHTCCAgUCFFw5T+2r/PXbAFYdJUeCO9NurTMhMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDE0NzIwWhcNMzUw +NDE0MDE0NzIwWjBLMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMQ0wCwYDVQQDDARXVURDMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAntyNuLTcMjWIgfrSPi+pvf3hNsHLzlV1 +Ze+hXvqs4fE0p36qVbWRUkwv+Vp4drKrlXrY5Em7JCNZ0IT9PuZ3PwFbnG5FwV+O +H3B7WrRx4JHhLZU3w+WHFGfWFDZS2eYWrf1f1tDOlyq92xx0QEqDNE+gUMJ+z1N7 +5yoDForShgTmxD+KzYp4e7BvKKKn/U6YosBC8GyiMWdcka0IUMoTwpQRQ3Vlx/KX ++YGJlwWA9wvNWOqgcYCxeCIa7X+8K9i92Py01T82zqLMcr52Ri2JEdMzHz4pp/aZ +d56pfUhGuN6ET18fTTGeKuB1PrPu80OgfOYvJL9u1/twuO/xi/naBQIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBAQAudZAxVxMh/aUeTj6WK5mpVpUINmAwt938FVSQ3nUC +mTafNjSNfu/Me+PvjCkmf9RNk5BSdHVwxseOBaU0WZnvLM056ayyQnSwWT53WwtZ +CSb3VHBbwaJqAiAnhQxCHHhWDK4rtIPrWZ/SCek2E5l29ccZHTuVnaFF3pG8YRs1 ++UXVWeCB0fIJwQdugtGkzVzgO1HW7tKyG1BqCP9q9FnfODia9d6jKAA5dScBPanG +yPNI8eq4Fpk4R6P1jM2jw9pCJ4CW4lpgGQD0KATJve+ooHJqL3Ri6xkljNymGfbP +UD8eyRxgUaKUloQY2UbafzO+cLn2uRiB4sfn/T8hybeW +-----END CERTIFICATE----- +Bag Attributes + friendlyName: devlog-app + localKeyID: 7A 75 ED 96 03 AC E0 78 BB DB F3 B9 DD 89 90 6D 4C 76 C7 8C +Key Attributes: +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCSwxp2L1CJnquR +WZ96oZGMnYM3OQBE6IJ74WLOLA36rzZWXwtWW3zPcMIaKUw6QLA0OnxBKWnXndAz +LSXrJXObEoNflBqTI0eVAkY7sB+bt1sIzMgjFtjkusu0bNs70etz6G6lU+dX+d/j +/E+45yrHAFNP9w5JprJrkodqNeQGMNdE82s+PnS0N/SBA/5/ViIJ3/BVpO/xe67r +ItrsDl2cBodZqWwCaEjFUd6C3RsC8nsLMyoVxJqsYkQE4SMowdd1fazno2vnpIaz +EBuQ6ujIi1YaCu+V/E6SRXs2GDr7y8lzXUE9brJnqz7qaQq8HgMqN1phdKwax5z/ +3xzoJgUBAgMBAAECggEAB0rjjzjVpyj3vH64EndhzJttEDroXQQyq6Ys6zK8NRcs +u4j4fr+ICaTAOF2R+JkLSGUZlIFSzZB9bnWRW0heoLeASKkK0wHfRjO5OrELOQkY +4GyQi1HQ0DjJ83qvQB8ztGw5x0ROjAwSCHmamoT+FqpY+XG8x4MdfYPn76qi3H3Q +iRs5SqzksnYL5irsdOLh84FwJuXhQzZVaBFnK38P2xWNRS2uiJYQoDjEavYcRoLr +ZmoZgtyLn7cMXRD177y2KhixhLReJqiIP3fX2SZCD8sGOzvLxlcUQh/cusIyw+4M +t5fAb3G3kpxey6IpeWwriXBVhPlv/t0h1rB3m+3jSwKBgQDC8fXRAm6GVIoTAjiY +ipxFu88u2KDhfN+HRzLKQwy3lcl9V4qFybI1oR2HxllMyB03+gtcxT2gFe4AI6az +KUZSjjYtzGqSxeus4cFc4Ec11LJbPk5fb2CnRLhQdV748YvWX4P/ZXkcI1mu34k0 +namLtM/jFvJQ+7uvGBkihK580wKBgQDAuf3EiOqDFG2qczaTcqzS4VqPYtXmCqw4 +vwjZNRMmvXNzNly2AuTA7ham3QTKDcXmeaF/zmFxWZhNdiIbW0oolraQ9ZNrqZap +eKBHrAhEOTdF6ZwEXjK88c0/TOUA8HC3enqlO6qrHNIPtUTsR+lTmlyo/SiLSWiO +wnKqSoICWwKBgCt5+vCaMjwTLpf+rtCWWTPUJuizt22Sg+ePoWwqd/OZnE4v79zW +lsAPJp7ZRaEyIBIT2eTeuFezjFjLmqnqUpymyr58EGiba2wrDQzBmCARR5XB14jB +NjUXxmNrSbsLY7xzoOScpN35pE6z2824O8/Ei3iB7ZjSC5GJNlHUdXWxAoGAJO0+ +F0MYk+b9IDSVF2lYfctZ+7E3RK101CaePmfx9HFGRqP63ZDuXZ0A0BX3DfPXoFJb +xE4502sUSHtDC7TRH7fI4Tt8dJt4153aMAFhUBkaYxXgo+GcnSFDb0Z/dk+beTxJ +dZFaIRETmpjjzNX2eeNQr7xZ4V4+X2QYblJ6WJMCgYEAnz7BSZgwauHJaQ2zUuRC +nwsrQ8x2Lb6oUtl517F5L9KdDCuZDhDT/fR5i78GRQUBRJAeA1TeKeA1oSCn7uI8 +Sf+cnkc3LrfgesTE5cg10qsNZ9WJV0uZto0Fi0Ku6JlLFIGn64rMedyyTbXoDlCY +gH32hd6WfH01of4nr/ShfD8= +-----END PRIVATE KEY----- diff --git a/certs/app_certs/naver.crt b/certs/app_certs/naver.crt new file mode 100644 index 0000000..9bcae38 --- /dev/null +++ b/certs/app_certs/naver.crt @@ -0,0 +1,42 @@ +-----BEGIN CERTIFICATE----- +MIIHVjCCBt2gAwIBAgIQBi5XjUxQQ8sxGGoah/DeHzAKBggqhkjOPQQDAzBWMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTAwLgYDVQQDEydEaWdp +Q2VydCBUTFMgSHlicmlkIEVDQyBTSEEzODQgMjAyMCBDQTEwHhcNMjUwMzA1MDAw +MDAwWhcNMjYwMzE3MjM1OTU5WjBlMQswCQYDVQQGEwJLUjEUMBIGA1UECBMLR3ll +b25nZ2ktZG8xFDASBgNVBAcTC1Nlb25nbmFtLXNpMRQwEgYDVQQKEwtOQVZFUiBD +b3JwLjEUMBIGA1UEAwwLKi5uYXZlci5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMB +BwNCAARwbkPreVaks3onKHkhrKVnU2j1DaZfqIXviUhkLoZZSWBUSyhROzT17eyv +Of5BLlim5cY6hR2n37rbHm9c2kMPo4IFfDCCBXgwHwYDVR0jBBgwFoAUCrwIKReM +pTlteg7OM8cus+37w3owHQYDVR0OBBYEFIyPahhzclG4UZZ7U2d8VizKld9BMIIC +DgYDVR0RBIICBTCCAgGCCyoubmF2ZXIuY29tggsqLm5hdmVyLm5ldIISKi5zZWFy +Y2gubmF2ZXIuY29tghAqLnZldGEubmF2ZXIuY29tgg8qLm5pZC5uYXZlci5jb22C +ESoudGVybXMubmF2ZXIuY29tghMqLnN3aW5kb3cubmF2ZXIuY29tghEqLnN0b3Jl +Lm5hdmVyLmNvbYIRKi5zdG9jay5uYXZlci5jb22CEiouc3BvcnRzLm5hdmVyLmNv +bYIUKi5zaG9wcGluZy5uYXZlci5jb22CECoubmV3cy5uYXZlci5jb22CECoucG9z +dC5uYXZlci5jb22CECouYmxvZy5uYXZlci5jb22CDyoua2luLm5hdmVyLmNvbYIT +Ki5maW5hbmNlLm5hdmVyLmNvbYIVKi5lbnRlcnRhaW4ubmF2ZXIuY29tghAqLmRp +Y3QubmF2ZXIuY29tghYqLmNvbW1lbnRib3gubmF2ZXIuY29tghMqLmNvbW1lbnQu +bmF2ZXIuY29tghEqLmNvbWljLm5hdmVyLmNvbYIQKi5jYWZlLm5hdmVyLmNvbYIO +Ki5hZC5uYXZlci5jb22CEiouZXhwZXJ0Lm5hdmVyLmNvbYINKi5tLm5hdmVyLmNv +bYIQKi5saWtlLm5hdmVyLmNvbYIaKi5pbXByZXNzaW9uLW5lby5uYXZlci5jb20w +PgYDVR0gBDcwNTAzBgZngQwBAgIwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5k +aWdpY2VydC5jb20vQ1BTMA4GA1UdDwEB/wQEAwIDiDAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwgZsGA1UdHwSBkzCBkDBGoESgQoZAaHR0cDovL2NybDMu +ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTSHlicmlkRUNDU0hBMzg0MjAyMENBMS0x +LmNybDBGoESgQoZAaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExT +SHlicmlkRUNDU0hBMzg0MjAyMENBMS0xLmNybDCBhQYIKwYBBQUHAQEEeTB3MCQG +CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wTwYIKwYBBQUHMAKG +Q2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU0h5YnJpZEVD +Q1NIQTM4NDIwMjBDQTEtMS5jcnQwDAYDVR0TAQH/BAIwADCCAX8GCisGAQQB1nkC +BAIEggFvBIIBawFpAHYADleUvPOuqT4zGyyZB7P3kN+bwj1xMiXdIaklrGHFTiEA +AAGVZImplQAABAMARzBFAiEAuYk/yx3umm8H+XrXNcgP1if28lxiHNYLfG8kdf5t +ytUCIHCJRMX2+NLEBLc33zsSLhCD6uim96ZzeAsGMbxfI8izAHYAZBHEbKQS7KeJ +HKICLgC8q08oB9QeNSer6v7VA8l9zfAAAAGVZImp1QAABAMARzBFAiEAtjwIDf5+ +E4Tbiu8zvK0U3xKGjKTd2fQ7F5nhs1N7zcQCIBG4fBRor6GqfkYz8LDtHi9r93dv +loatNSKac1+je1GxAHcASZybad4dfOz8Nt7Nh2SmuFuvCoeAGdFVUvvp6ynd+MMA +AAGVZImp4wAABAMASDBGAiEAs/xntzku3+3gRoAYmj5tTFVWikLu13W+2ZjHzdp2 +jCsCIQDsaHK22U1wY+ZbqEVK1foz9++oKsDuZDlOqd4hZZn0OzAKBggqhkjOPQQD +AwNnADBkAjAclZurq/yDjWZHjlmNDckGmXrcUlidhqJ8RofrAR/SE5i4yyWhM35Y +n14mcDi/6GkCMBwbBr+AwCHY1k67nYGIsxuQAt7VWON2q73jl7kqQ8q79WR2g89a +J7WShHkL3R2VCw== +-----END CERTIFICATE----- diff --git a/certs/app_certs/server-cert.pem b/certs/app_certs/server-cert.pem new file mode 100644 index 0000000..e9c6e0f --- /dev/null +++ b/certs/app_certs/server-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDIzCCAgsCFFhWSQG3H1J81Xt7M+sUWjv4RQLAMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDIzODQ4WhcNMjYw +NDE2MDIzODQ4WjBRMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMRMwEQYDVQQDDApkZXZsb2ctYXBwMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAksMadi9QiZ6rkVmfeqGRjJ2D +NzkAROiCe+FiziwN+q82Vl8LVlt8z3DCGilMOkCwNDp8QSlp153QMy0l6yVzmxKD +X5QakyNHlQJGO7Afm7dbCMzIIxbY5LrLtGzbO9Hrc+hupVPnV/nf4/xPuOcqxwBT +T/cOSaaya5KHajXkBjDXRPNrPj50tDf0gQP+f1YiCd/wVaTv8Xuu6yLa7A5dnAaH +WalsAmhIxVHegt0bAvJ7CzMqFcSarGJEBOEjKMHXdX2s56Nr56SGsxAbkOroyItW +GgrvlfxOkkV7Nhg6+8vJc11BPW6yZ6s+6mkKvB4DKjdaYXSsGsec/98c6CYFAQID +AQABMA0GCSqGSIb3DQEBCwUAA4IBAQCNlBmDRL3e1ryjlOEgQSa49YCjDHGSrWli +PKYvdL4pJa9XXJFXssSvKRXfGlSfva+VVGkouqSbYLdwNEF1ixp3G6RGdwq14yZw ++I6bi/76Yops/JTRsekLcV3E/oqKuaIMrdo8RgB6kRmxmMNnB1+Zb9amNEc+mduM +1L7r8nPyUWsxLAUjCoW/W5KMU1JjVxm7PRJmnrBvRzA8eXnxXJBesZMJ2fUAMJCt +RvGTRy5f/M1Xb8iJw6O2zu3FAszn+eZBHsXsy1z45vNNoWH+DTFf+kCuTJqiPwNA +/4tYRkZIUbVg6NlrCSqFyxRYRY3R/hujpekvtE3W9cFBCsiDHLbp +-----END CERTIFICATE----- diff --git a/certs/app_certs/server-full-chain.pem b/certs/app_certs/server-full-chain.pem new file mode 100644 index 0000000..aab39ab --- /dev/null +++ b/certs/app_certs/server-full-chain.pem @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIDIzCCAgsCFFhWSQG3H1J81Xt7M+sUWjv4RQLAMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDIzODQ4WhcNMjYw +NDE2MDIzODQ4WjBRMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMRMwEQYDVQQDDApkZXZsb2ctYXBwMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAksMadi9QiZ6rkVmfeqGRjJ2D +NzkAROiCe+FiziwN+q82Vl8LVlt8z3DCGilMOkCwNDp8QSlp153QMy0l6yVzmxKD +X5QakyNHlQJGO7Afm7dbCMzIIxbY5LrLtGzbO9Hrc+hupVPnV/nf4/xPuOcqxwBT +T/cOSaaya5KHajXkBjDXRPNrPj50tDf0gQP+f1YiCd/wVaTv8Xuu6yLa7A5dnAaH +WalsAmhIxVHegt0bAvJ7CzMqFcSarGJEBOEjKMHXdX2s56Nr56SGsxAbkOroyItW +GgrvlfxOkkV7Nhg6+8vJc11BPW6yZ6s+6mkKvB4DKjdaYXSsGsec/98c6CYFAQID +AQABMA0GCSqGSIb3DQEBCwUAA4IBAQCNlBmDRL3e1ryjlOEgQSa49YCjDHGSrWli +PKYvdL4pJa9XXJFXssSvKRXfGlSfva+VVGkouqSbYLdwNEF1ixp3G6RGdwq14yZw ++I6bi/76Yops/JTRsekLcV3E/oqKuaIMrdo8RgB6kRmxmMNnB1+Zb9amNEc+mduM +1L7r8nPyUWsxLAUjCoW/W5KMU1JjVxm7PRJmnrBvRzA8eXnxXJBesZMJ2fUAMJCt +RvGTRy5f/M1Xb8iJw6O2zu3FAszn+eZBHsXsy1z45vNNoWH+DTFf+kCuTJqiPwNA +/4tYRkZIUbVg6NlrCSqFyxRYRY3R/hujpekvtE3W9cFBCsiDHLbp +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDHTCCAgUCFFw5T+2r/PXbAFYdJUeCO9NurTMhMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDE0NzIwWhcNMzUw +NDE0MDE0NzIwWjBLMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMQ0wCwYDVQQDDARXVURDMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAntyNuLTcMjWIgfrSPi+pvf3hNsHLzlV1 +Ze+hXvqs4fE0p36qVbWRUkwv+Vp4drKrlXrY5Em7JCNZ0IT9PuZ3PwFbnG5FwV+O +H3B7WrRx4JHhLZU3w+WHFGfWFDZS2eYWrf1f1tDOlyq92xx0QEqDNE+gUMJ+z1N7 +5yoDForShgTmxD+KzYp4e7BvKKKn/U6YosBC8GyiMWdcka0IUMoTwpQRQ3Vlx/KX ++YGJlwWA9wvNWOqgcYCxeCIa7X+8K9i92Py01T82zqLMcr52Ri2JEdMzHz4pp/aZ +d56pfUhGuN6ET18fTTGeKuB1PrPu80OgfOYvJL9u1/twuO/xi/naBQIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBAQAudZAxVxMh/aUeTj6WK5mpVpUINmAwt938FVSQ3nUC +mTafNjSNfu/Me+PvjCkmf9RNk5BSdHVwxseOBaU0WZnvLM056ayyQnSwWT53WwtZ +CSb3VHBbwaJqAiAnhQxCHHhWDK4rtIPrWZ/SCek2E5l29ccZHTuVnaFF3pG8YRs1 ++UXVWeCB0fIJwQdugtGkzVzgO1HW7tKyG1BqCP9q9FnfODia9d6jKAA5dScBPanG +yPNI8eq4Fpk4R6P1jM2jw9pCJ4CW4lpgGQD0KATJve+ooHJqL3Ri6xkljNymGfbP +UD8eyRxgUaKUloQY2UbafzO+cLn2uRiB4sfn/T8hybeW +-----END CERTIFICATE----- diff --git a/certs/app_certs/server-key.pem b/certs/app_certs/server-key.pem new file mode 100644 index 0000000..c0b3f31 --- /dev/null +++ b/certs/app_certs/server-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCSwxp2L1CJnquR +WZ96oZGMnYM3OQBE6IJ74WLOLA36rzZWXwtWW3zPcMIaKUw6QLA0OnxBKWnXndAz +LSXrJXObEoNflBqTI0eVAkY7sB+bt1sIzMgjFtjkusu0bNs70etz6G6lU+dX+d/j +/E+45yrHAFNP9w5JprJrkodqNeQGMNdE82s+PnS0N/SBA/5/ViIJ3/BVpO/xe67r +ItrsDl2cBodZqWwCaEjFUd6C3RsC8nsLMyoVxJqsYkQE4SMowdd1fazno2vnpIaz +EBuQ6ujIi1YaCu+V/E6SRXs2GDr7y8lzXUE9brJnqz7qaQq8HgMqN1phdKwax5z/ +3xzoJgUBAgMBAAECggEAB0rjjzjVpyj3vH64EndhzJttEDroXQQyq6Ys6zK8NRcs +u4j4fr+ICaTAOF2R+JkLSGUZlIFSzZB9bnWRW0heoLeASKkK0wHfRjO5OrELOQkY +4GyQi1HQ0DjJ83qvQB8ztGw5x0ROjAwSCHmamoT+FqpY+XG8x4MdfYPn76qi3H3Q +iRs5SqzksnYL5irsdOLh84FwJuXhQzZVaBFnK38P2xWNRS2uiJYQoDjEavYcRoLr +ZmoZgtyLn7cMXRD177y2KhixhLReJqiIP3fX2SZCD8sGOzvLxlcUQh/cusIyw+4M +t5fAb3G3kpxey6IpeWwriXBVhPlv/t0h1rB3m+3jSwKBgQDC8fXRAm6GVIoTAjiY +ipxFu88u2KDhfN+HRzLKQwy3lcl9V4qFybI1oR2HxllMyB03+gtcxT2gFe4AI6az +KUZSjjYtzGqSxeus4cFc4Ec11LJbPk5fb2CnRLhQdV748YvWX4P/ZXkcI1mu34k0 +namLtM/jFvJQ+7uvGBkihK580wKBgQDAuf3EiOqDFG2qczaTcqzS4VqPYtXmCqw4 +vwjZNRMmvXNzNly2AuTA7ham3QTKDcXmeaF/zmFxWZhNdiIbW0oolraQ9ZNrqZap +eKBHrAhEOTdF6ZwEXjK88c0/TOUA8HC3enqlO6qrHNIPtUTsR+lTmlyo/SiLSWiO +wnKqSoICWwKBgCt5+vCaMjwTLpf+rtCWWTPUJuizt22Sg+ePoWwqd/OZnE4v79zW +lsAPJp7ZRaEyIBIT2eTeuFezjFjLmqnqUpymyr58EGiba2wrDQzBmCARR5XB14jB +NjUXxmNrSbsLY7xzoOScpN35pE6z2824O8/Ei3iB7ZjSC5GJNlHUdXWxAoGAJO0+ +F0MYk+b9IDSVF2lYfctZ+7E3RK101CaePmfx9HFGRqP63ZDuXZ0A0BX3DfPXoFJb +xE4502sUSHtDC7TRH7fI4Tt8dJt4153aMAFhUBkaYxXgo+GcnSFDb0Z/dk+beTxJ +dZFaIRETmpjjzNX2eeNQr7xZ4V4+X2QYblJ6WJMCgYEAnz7BSZgwauHJaQ2zUuRC +nwsrQ8x2Lb6oUtl517F5L9KdDCuZDhDT/fR5i78GRQUBRJAeA1TeKeA1oSCn7uI8 +Sf+cnkc3LrfgesTE5cg10qsNZ9WJV0uZto0Fi0Ku6JlLFIGn64rMedyyTbXoDlCY +gH32hd6WfH01of4nr/ShfD8= +-----END PRIVATE KEY----- diff --git a/certs/app_certs/server.csr b/certs/app_certs/server.csr new file mode 100644 index 0000000..287ffa6 --- /dev/null +++ b/certs/app_certs/server.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICljCCAX4CAQAwUTELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYD +VQQHDAVQdXNhbjENMAsGA1UECgwEV1VEQzETMBEGA1UEAwwKZGV2bG9nLWFwcDCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJLDGnYvUImeq5FZn3qhkYyd +gzc5AETognvhYs4sDfqvNlZfC1ZbfM9wwhopTDpAsDQ6fEEpaded0DMtJeslc5sS +g1+UGpMjR5UCRjuwH5u3WwjMyCMW2OS6y7Rs2zvR63PobqVT51f53+P8T7jnKscA +U0/3DkmmsmuSh2o15AYw10Tzaz4+dLQ39IED/n9WIgnf8FWk7/F7rusi2uwOXZwG +h1mpbAJoSMVR3oLdGwLyewszKhXEmqxiRAThIyjB13V9rOeja+ekhrMQG5Dq6MiL +VhoK75X8TpJFezYYOvvLyXNdQT1usmerPuppCrweAyo3WmF0rBrHnP/fHOgmBQEC +AwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBQgaCS/3NRrDwYmhYSInyDgNiqrz+V +txsvjJGLi+eWsG8538qvDYAjmjlAUZkP48Dhc4n9Wx0nkc8i+n5HjJ60nqBHu6eQ +mKcNmudEzGewF7XTyPY11AT3GNJqOqtek6kbhauxFCby2Iuz8B8obe8EeDhJ4TZ7 +qs6Y96l/ocv3AFDrRNk6kVkaTPyMhjHGvg5BZuJRZfdnGNXWWaY1m3kumGj/4zTR +smiqcc0uQSnqAPY1MO/9qIxrh/1wF0+LQL2HTQAtz3DWu5EUZFRiktnyWOGdibbh ++3ACKB7ixq+7nKQEgzEg49DTFSgwByAt1VWaqiumj4A7x+h64PZFNc8S +-----END CERTIFICATE REQUEST----- diff --git a/certs/app_certs/truststore.p12 b/certs/app_certs/truststore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..b70d1cd026491fbc7084a3472edffe156918959c GIT binary patch literal 16294 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 ZQTu}?Tv?De=|zqCT=J??=l@mY{|8{Cs!0F< literal 0 HcmV?d00001 diff --git a/certs/app_certs/truststore.pem b/certs/app_certs/truststore.pem new file mode 100644 index 0000000..b992733 --- /dev/null +++ b/certs/app_certs/truststore.pem @@ -0,0 +1,147 @@ +Bag Attributes + friendlyName: devlog-ca + 2.16.840.1.113894.746875.1.1: +subject=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = WUDC +issuer=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = WUDC +-----BEGIN CERTIFICATE----- +MIIDHTCCAgUCFFw5T+2r/PXbAFYdJUeCO9NurTMhMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDE0NzIwWhcNMzUw +NDE0MDE0NzIwWjBLMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMQ0wCwYDVQQDDARXVURDMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAntyNuLTcMjWIgfrSPi+pvf3hNsHLzlV1 +Ze+hXvqs4fE0p36qVbWRUkwv+Vp4drKrlXrY5Em7JCNZ0IT9PuZ3PwFbnG5FwV+O +H3B7WrRx4JHhLZU3w+WHFGfWFDZS2eYWrf1f1tDOlyq92xx0QEqDNE+gUMJ+z1N7 +5yoDForShgTmxD+KzYp4e7BvKKKn/U6YosBC8GyiMWdcka0IUMoTwpQRQ3Vlx/KX ++YGJlwWA9wvNWOqgcYCxeCIa7X+8K9i92Py01T82zqLMcr52Ri2JEdMzHz4pp/aZ +d56pfUhGuN6ET18fTTGeKuB1PrPu80OgfOYvJL9u1/twuO/xi/naBQIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBAQAudZAxVxMh/aUeTj6WK5mpVpUINmAwt938FVSQ3nUC +mTafNjSNfu/Me+PvjCkmf9RNk5BSdHVwxseOBaU0WZnvLM056ayyQnSwWT53WwtZ +CSb3VHBbwaJqAiAnhQxCHHhWDK4rtIPrWZ/SCek2E5l29ccZHTuVnaFF3pG8YRs1 ++UXVWeCB0fIJwQdugtGkzVzgO1HW7tKyG1BqCP9q9FnfODia9d6jKAA5dScBPanG +yPNI8eq4Fpk4R6P1jM2jw9pCJ4CW4lpgGQD0KATJve+ooHJqL3Ri6xkljNymGfbP +UD8eyRxgUaKUloQY2UbafzO+cLn2uRiB4sfn/T8hybeW +-----END CERTIFICATE----- +Bag Attributes + friendlyName: mysql-server + 2.16.840.1.113894.746875.1.1: +subject=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = devlog-mysql +issuer=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = WUDC +-----BEGIN CERTIFICATE----- +MIIDJTCCAg0CFFhWSQG3H1J81Xt7M+sUWjv4RQK+MA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDIwMTA4WhcNMzUw +NDE0MDIwMTA4WjBTMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMRUwEwYDVQQDDAxkZXZsb2ctbXlzcWww +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDoOhJ5apnT8jlYjtVh7rgh +F2DKtabV+H1gEq6NxsrzLlfzfn1BqC4OCznCZArYcTPjLLwfNGVsRzsVtm/mWSCY +GKffPURUHW/TqnRHhFw0GXB63NK8dEdvzQib7S5esvLmT+u9VSNgfNQCvpw3fyQD +akaGk2GP+pKU1Snmp7stmjUDv8G3UIVePfhizXvMqI2LnfDgAUnQioonAoyksq5s +6GMrUoJZvjVeJutt6Kec8sguAdOb/yvMQ7BBbweAZqwkgGkVz0gQUzJH79BLPbsq +FbHzpBs5pBajk1yrg9bRJe0hPDcLwWXqczr5tSxLHv966PhK98j1XA+Acq77ljod +AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAJIUK9L5sCg9U09p7FbwQbLdVB7p45xT +8NKkOpDxEN+kFacNkbbkL3pD64T/3dQ4LWNiWV2WlBdqJoMIl1Rafcd+Oj6OqiTW +l9ICl/dhul+Z3MGXGdvgfc49vHO4SPc/ijMzowA7+3oiopOVIsid7wVCxZDeueVQ +HJQiqyVMC6vTZBGfoMzbUcOZn+MJV8Q9Fnh3kcx+clhAYiKsh52RKrDGhZrRzvoF +cLuOJlL5IcHYMBRxfoeYdoD200qG2TZb7GnIxb/vvzeLIGYwb80amuXkHaNuAqNY +rfFOMk4EAg3UChDquFDkD6nhP3zgPTiLULHSVbPz54sV7RHtzW1xUHo= +-----END CERTIFICATE----- +Bag Attributes + friendlyName: intermediate + 2.16.840.1.113894.746875.1.1: +subject=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = WUDC +issuer=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = WUDC +-----BEGIN CERTIFICATE----- +MIIDHTCCAgUCFFw5T+2r/PXbAFYdJUeCO9NurTMhMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDE0NzIwWhcNMzUw +NDE0MDE0NzIwWjBLMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMQ0wCwYDVQQDDARXVURDMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAntyNuLTcMjWIgfrSPi+pvf3hNsHLzlV1 +Ze+hXvqs4fE0p36qVbWRUkwv+Vp4drKrlXrY5Em7JCNZ0IT9PuZ3PwFbnG5FwV+O +H3B7WrRx4JHhLZU3w+WHFGfWFDZS2eYWrf1f1tDOlyq92xx0QEqDNE+gUMJ+z1N7 +5yoDForShgTmxD+KzYp4e7BvKKKn/U6YosBC8GyiMWdcka0IUMoTwpQRQ3Vlx/KX ++YGJlwWA9wvNWOqgcYCxeCIa7X+8K9i92Py01T82zqLMcr52Ri2JEdMzHz4pp/aZ +d56pfUhGuN6ET18fTTGeKuB1PrPu80OgfOYvJL9u1/twuO/xi/naBQIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBAQAudZAxVxMh/aUeTj6WK5mpVpUINmAwt938FVSQ3nUC +mTafNjSNfu/Me+PvjCkmf9RNk5BSdHVwxseOBaU0WZnvLM056ayyQnSwWT53WwtZ +CSb3VHBbwaJqAiAnhQxCHHhWDK4rtIPrWZ/SCek2E5l29ccZHTuVnaFF3pG8YRs1 ++UXVWeCB0fIJwQdugtGkzVzgO1HW7tKyG1BqCP9q9FnfODia9d6jKAA5dScBPanG +yPNI8eq4Fpk4R6P1jM2jw9pCJ4CW4lpgGQD0KATJve+ooHJqL3Ri6xkljNymGfbP +UD8eyRxgUaKUloQY2UbafzO+cLn2uRiB4sfn/T8hybeW +-----END CERTIFICATE----- +Bag Attributes + friendlyName: root + 2.16.840.1.113894.746875.1.1: +subject=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = WUDC +issuer=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = WUDC +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIUJQjfesPiPpsGhsWZH4oTggUWVD8wDQYJKoZIhvcNAQEL +BQAwSzELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYDVQQHDAVQdXNh +bjENMAsGA1UECgwEV1VEQzENMAsGA1UEAwwEV1VEQzAeFw0yNTA0MTYwMTQzMDda +Fw0zNTA0MTQwMTQzMDdaMEsxCzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEO +MAwGA1UEBwwFUHVzYW4xDTALBgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDczXj3rCSwS+cv2FXTRWxcLxuE +m0DY9RqQfUZ6XM24IuBpxeicqIcGs8lYSWmx9cRFnxDG5beV5BSBOt3MoR9Q5Caf +Ld4JWHEIXicDk78vQjTE7QB1xPkieU0s4wqh9W0rPKEYsJ8SEhFgMUq5g5EiaVqQ +PRaaEeK1C0Zn0e5NdwcMgJ6E6uwij9E1U95QAw6AFyvqoS+s/1TsEcAkN3oIXdlD +kYy7lO4uLZI4Dfh77kfAf3LxKN/qE6WE20wyzipwb7yXFVzcTm5WoLSwV4axWrYF +Bc67x3X5t6K985Y1f5UMfnKq+JRAl0wFC0SOhS8NXHFVNQCbXvwldfcUSwc/AgMB +AAGjUzBRMB0GA1UdDgQWBBRa5Q7LNg15ZZYZSMQoowHLNtJtezAfBgNVHSMEGDAW +gBRa5Q7LNg15ZZYZSMQoowHLNtJtezAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQDXLVSeVyhCuw4oJoRw/wPwQEeBScxmpkSQvMzl19QhFXkxZxUT +5vhkCJc3lp1Ifa405n5I1+WFVhWyF1cSTXYddxz/eLjW1p54VNPXm5nxKzwwyftE +Ps36G32D0kdkk9pRqnsQ2Dt5j8hf2RVXzyswu97mTkRXTQMDwKUtwopr6HlIZiOY +BzUOEJf4NQr3VeGkXeWfqjWMU7ch7FWf7SsyqNFinAgiDLaIdEYSRBi8xiRJpOn4 +BUbhAlS2L4uGPWGa7TZ8sf8/s4Mdh784NSFn+4D6pVa7Quq5e6OmBVK4J851zAic +7mQsPhJLXactGWdksrjBSMQS7E5R8LG+k0j3 +-----END CERTIFICATE----- +Bag Attributes + friendlyName: redis-server + 2.16.840.1.113894.746875.1.1: +subject=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = devlog-redis +issuer=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = WUDC +-----BEGIN CERTIFICATE----- +MIIDJTCCAg0CFFhWSQG3H1J81Xt7M+sUWjv4RQK/MA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDIyMzUzWhcNMjYw +NDE2MDIyMzUzWjBTMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMRUwEwYDVQQDDAxkZXZsb2ctcmVkaXMw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD6WlIHMwItBuZD2HLs0sJb +OwQTvZmeCvcqCkTCc19GSWTAO0yuw8aZlsaNFKlOamGknDXVzUOKOojNNAzOeW4Y +/sbG+AtgFiFLblRPGIbKs4fNJa9TMJkt7u69YNVgRLnXRHIcoaQz/Ah5+398Puyw +HFSZOYpAmcxVCalYTT3+NnX4+Umnq/a7YFZFiiHBKWvmhCc+zp6CFQ1JjLKb/bdN +YiNxQoQ4RQf5/FquuQT5ZigsLGRMnnKA1IB3nmLR4PklGGEyYYakh+mFBnmWGWny +uxOj/6Q9ZsCs1jsrwl/W1EtWqjFZpWCJmkJbnNXMCgtriz1sE3P/RqbAF/1H9exF +AgMBAAEwDQYJKoZIhvcNAQELBQADggEBACR9DxcobWtjqiiR3r7/jQYMdnUVwGkv +euMJXRCzzPVV1PkgEMotGTksoZ0GElProZo5TK/DiGmc5zi3Ql7rADk6blDk1kj3 +W0i9/rDwBkgC5r7NFHoxS/73yumAZsrIjz0sYlRgzUHc33jv7pb2RPmkFZ4Qj5By +R3W7/rN+9+GflTvMgVeTljPfyS5/Tt5svnp1oE97vd/TMPZgMF1KHYG4yaGQfztX +YnPYb2knshGeGWmCzNoPrQI3Ug3LKyXwN2f2QYqeCahGztBkZPp3yy+CNSqMtI0H +iSwkDA0dGd2Fk6w4ZI/o+QUvOwSgsxx96LXaffCGbSpdeJXflyFRopM= +-----END CERTIFICATE----- +Bag Attributes + friendlyName: nginx-server + 2.16.840.1.113894.746875.1.1: +subject=C = KR, ST = Pusan, L = Pusan, O = WUDC, OU = WUDC, CN = devlog-nginx, emailAddress = qqwwee771441@gmail.com +issuer=C = KR, ST = Pusan, L = Pusan, O = WUDC, CN = WUDC +-----BEGIN CERTIFICATE----- +MIIDXDCCAkQCFFw5T+2r/PXbAFYdJUeCO9NurTMiMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDMwMjQzWhcNMjYw +NDE2MDMwMjQzWjCBiTELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYD +VQQHDAVQdXNhbjENMAsGA1UECgwEV1VEQzENMAsGA1UECwwEV1VEQzEVMBMGA1UE +AwwMZGV2bG9nLW5naW54MSUwIwYJKoZIhvcNAQkBFhZxcXd3ZWU3NzE0NDFAZ21h +aWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8B8rbUMBDAUe +47l7uKtGTgwTAStVBueE69flhTnd4f2cOigIueXYqHPL8vukLHFkCvHYS2DSsTsd +ja/QhaqWQJK0f5H3by4KzbFgCfRbjWnw2LYTfk4zZo69gkCPbsaDPAZ2Q6XyLZ7Z +SBu+uTTHftVELieUu1HKjJCyfH7M9++cufM1V61EgODI5YEVcziyGuTZaQLeKi10 +5wMe57FcDq4ABNDjvmGAsJKzR5+z8W02f5OIacjcOVStlsyzvkrCEvJKcacgeH+s +RBA8tG8hUup8c13TjLQ0CM8GF7PigXRKYFfiLRqJju7ThNKKzozLJWknK69SMW61 +1tR3ohb90wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCb5BMY3Woz1Kxeog+7oHnv +wVSVemf+XbtRfkOQrFSlEFY10OKlIf4mT2PEkgmx8lfNHx+RGmv0w1g9N/VAw7zh +X1wr39GOrawzgWZLo7kLtfNQ2Q2IfLttTkiR2IlYPdGU0+c1LS1EnuVHgZbjwnyf +e4SAfA/zc2CKY3H23H7rm1Xr8aKKzLB/13Xc1kGEzQVucnoJyMPtwRDzWnFO5qoA +7d4ClDoYg3Wy6I02ZKKMkktKSfbp+AQBzvT1s+iPWookpdkco/DZCkO6tmyh6CdR +FaYUymw5MVElSCjRkzDQ0DhO5fZhu3w0VgaSfusRIfeXSGxUBhzrV7Z0Vjh2cFgQ +-----END CERTIFICATE----- diff --git a/certs/ca-key.pem b/certs/ca-key.pem new file mode 100644 index 0000000..d1ed693 --- /dev/null +++ b/certs/ca-key.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQItBgWYtKAT6gCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBP12tS4G9Uy8kzR1+xDHQQBIIE +0HqE3pxgavsUTSgWIKdVDHgaP/IEjon3Wau5OgAmy01oWH9OTxeFiKSXfhFlsIqt +jWbGzC8MqlZ11kQPVn4HdezaEVQ4xY3m35DJQf2CyesFXYd7rXTqtiwqsa0lmyps +IrL77eB9RzOMNnvLi2NvfCbRJ/YtqVeeh60d5uCzlIUF4pGJfaKPqq17q9by9S3m +a7+rpm4gH0RooxuRMinyQwG6haPF7cJv/LxkPc1nVAtlVFdWPctdoYDbEnOAZ6zD +Ajam/YwduVOmDaUlX7oQDkU/B0Nki9rGDr6yuPc1OVdFqFjDSeqVr+LWnvLRRwZX +Qx2K4rG361yFcWhOcydIJmtbrDLZM+QwII/UqiFwptyTj007ioQqEU0kBhrnyFeW +VRqiDrn6m/bCZQbyF3UlGCI1HVWgQnPMBgboOMQsqXeDjVwXwTYmSag/jJDIXs4d +yZjyf1RF5Y5Fa15zkHb04ANbvsHW+QDMBCtir8/R64AtjXXI52oBG62KNGlZ814r +VP519ml69ABFTHJbfzrSzc8er783heibe6NsKc4TeLZ3Eqz6es0kwc7Z9hUFUOy4 +zkow1zpAIleWtALrwUXFQO6EOmxQ+SfM7WIXmtRHwfEHuD3R0h0D2vvvYvInNGjU +RVjaLx1FQXcoA8AK9qXq0rHBgJx0TY+jsE9ltQtDx8f+aD7NO5Fb9ImTsPUN4sZ9 +R/ivOwF1q+I2hQ53D80uBHwNrBaZkmvDO/N/7cRww6sbB3Ur5iGIp1DR0a3hvtpE +mQ2LEJIE5G4RpXDfdvpBzYtLQQsNI3Op16GN6ggugjq+1qlN43APseSpiUSh+evu +9ZTduUVr4y4ypT8jJD1CUbspP9MQShCkrQ6XGGh1cDX8kvKMKrPTdpApdv8daUZY +GGpeNgKw1yGQB/up/okaFrDvD3c5MpH9HyvGOLERV8dFNgspkpsZFC3bMHoIE9qJ +83PB7rleLyMHmQFfvwL2FtklaWqw98H+Fjh/AzXDwvj6Wfcvn7DhhdESvF1elG/W +QdBIhsVcoY9iDxcl9qsjIY21onsq5hy11iya7p/frUnFpmLbr7b9BJ3ZbAKh51C4 +6TB6gKQxOzjQUMAsgIvIn/H4spdvAZ3bopcED0G8MjC0hRFmfbs2d5pf2x07P1H4 +WG78zHTuEZRTBYi+th7K2xBhwpDs6Lh2KBhJAc21NN6AWhaGQBA3kqxrIkGNTlVX +SjV9hXrqG/x86SUpzcxY2yG4Y0FzBhHaHYNxmE0VvfrOgdBmAc9sYkYlZGo4X9wC +Zg8amOAQgjyLgmr+zIVJ4oUAemEAl/qqkKvoI/Z7LnO4QYqI0VaQkEMLX9m7NBaZ +XKVWbwauqJ8VBK0qBLqIC8a+rb8qZGwHnuBBtTTUmRmkPz+uCvbJacfi2lQ21Mcq +Mh32Gqy8HnGd+j1FX8QiFjknDRTfsAOtwOBzIlVUiwJn0RuEGImYOyKjqjiptXNR +0XdGlkVLM8Y3KsO6RL5842sYjE63/kxAkczfq8MQ0D7YMrZa7hZ42Syr7siYC/EL +lDxqg+yen80CeZ6V1aD7MVmCb7kQmphge2PCAmr8dHlfNytcub2TgrI2Lmc80jEn +Pw2s2YKpLIjUrykKp91XAqswb6V3Kmqt7JaAPtKqdv5u +-----END ENCRYPTED PRIVATE KEY----- diff --git a/certs/ca.pem b/certs/ca.pem new file mode 100644 index 0000000..0f8f29c --- /dev/null +++ b/certs/ca.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIUJQjfesPiPpsGhsWZH4oTggUWVD8wDQYJKoZIhvcNAQEL +BQAwSzELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYDVQQHDAVQdXNh +bjENMAsGA1UECgwEV1VEQzENMAsGA1UEAwwEV1VEQzAeFw0yNTA0MTYwMTQzMDda +Fw0zNTA0MTQwMTQzMDdaMEsxCzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEO +MAwGA1UEBwwFUHVzYW4xDTALBgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDczXj3rCSwS+cv2FXTRWxcLxuE +m0DY9RqQfUZ6XM24IuBpxeicqIcGs8lYSWmx9cRFnxDG5beV5BSBOt3MoR9Q5Caf +Ld4JWHEIXicDk78vQjTE7QB1xPkieU0s4wqh9W0rPKEYsJ8SEhFgMUq5g5EiaVqQ +PRaaEeK1C0Zn0e5NdwcMgJ6E6uwij9E1U95QAw6AFyvqoS+s/1TsEcAkN3oIXdlD +kYy7lO4uLZI4Dfh77kfAf3LxKN/qE6WE20wyzipwb7yXFVzcTm5WoLSwV4axWrYF +Bc67x3X5t6K985Y1f5UMfnKq+JRAl0wFC0SOhS8NXHFVNQCbXvwldfcUSwc/AgMB +AAGjUzBRMB0GA1UdDgQWBBRa5Q7LNg15ZZYZSMQoowHLNtJtezAfBgNVHSMEGDAW +gBRa5Q7LNg15ZZYZSMQoowHLNtJtezAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQDXLVSeVyhCuw4oJoRw/wPwQEeBScxmpkSQvMzl19QhFXkxZxUT +5vhkCJc3lp1Ifa405n5I1+WFVhWyF1cSTXYddxz/eLjW1p54VNPXm5nxKzwwyftE +Ps36G32D0kdkk9pRqnsQ2Dt5j8hf2RVXzyswu97mTkRXTQMDwKUtwopr6HlIZiOY +BzUOEJf4NQr3VeGkXeWfqjWMU7ch7FWf7SsyqNFinAgiDLaIdEYSRBi8xiRJpOn4 +BUbhAlS2L4uGPWGa7TZ8sf8/s4Mdh784NSFn+4D6pVa7Quq5e6OmBVK4J851zAic +7mQsPhJLXactGWdksrjBSMQS7E5R8LG+k0j3 +-----END CERTIFICATE----- diff --git a/certs/ca.srl b/certs/ca.srl new file mode 100644 index 0000000..301625b --- /dev/null +++ b/certs/ca.srl @@ -0,0 +1 @@ +5C394FEDABFCF5DB00561D2547823BD36EAD3324 diff --git a/certs/intermediate-key.pem b/certs/intermediate-key.pem new file mode 100644 index 0000000..09be3f5 --- /dev/null +++ b/certs/intermediate-key.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQI34aCVMAjVvsCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCGo3xYFwkXwd4iDaH5F2laBIIE +0JhoFyQ3IqFBMptzo9psmvEnhkrogK/UMnMxU7t3ttgxM1G/OhRO3P0pzbKSrpLC +sSys0TnXiz0Lr4Mfj/kLMme9XUdWqO+blM+bJx0XcXql38o2tFD5OkUfYFUrlz8d +MD1RWqVmou+dEe2evSl3VH1cjl1gN3zaCakGJKzRCdrQtLbt/9cnPOaN+sRvsx3i +rC1P9D9xtlLhCSoMi30LG7TXx64S2kQ2ErcUeT/9OQHDtQ/jSUHSgNBXzMola9Ds +BShTeozDcKp9bz85Xoa0OcJ2QhEJd8xZfTHRcKvPZ/kqDqI5NdfutlrIPnZv4c32 +o5trQ7IEBAv48a9QGEsYQDSG4mgzjje4vsGp9Y28kMjG39R3y2tvWcFCTeHQYf9X +6EJ4fyJqF7Ul4/kZUUQzj0mD/lmTh9kKKpNwOylfllowS3GzzytEZ1gbYY16fqcE +64zY3CvMJCnIBriq+UtUEg2hySJYyHyEFpIfs/HZ1+aEC7RsCwrEjKfRz82Ye6VB +RWDxL9pKF3QUnzMEqErsWBbRymb+jwlwoRJW9w9+H/4P61SYkL96R7SuTTdUIyW7 +RBpKZrwlNH7yfuXaLJbfESNpoq0nEKgzKZHfeX0wSf2dLsPp4CqttRF976J0lWb9 +Nk6F5k6khyp1zOpo2Te4luOjjtQxsIagOWvaPXerOqVvYXf3JCeSFjYJYqoklvNH +uE6ujzoHj7br2CIgf+ukW3EZqSqYuwvBOqAHwe0W7XabpRzcXzQJZW1y9B8gMhqW +XT623dkcANEEy7fjbOQT060sexTXM56T/GnE8pVZukSkPppaAAU83iMmY1O4f+GW +Y1nPenhwSVmlbqWTnD/9hbuubkBFxrv9/MTy8EvohlH+O5m7OvCGibMhtZYMArDc +hhfBNIHmTx06j41beFGZEAd7Eg6c1ckF+7eEKuWRxRIYRhL9wQpPkkO9O+JwFQE1 +Ut/0MW7l3b2z2DnfG/gtVtE8h7sn/lgW77suuhdkOzthFLDBjak2qtal0agGDRo7 +n5aSVBYxy3DCyX+Lk38NBr5OB3/x1oczztxs+sQ7rubTnRoXq8rk3incP3QJDZBW +Fi7uCPeeNSAhYMJcta5LxxSxUNGYaxPTlIXvJ1U5Dnxixi/W9nzprcevMJpxRCLH +WJLUjtjNKFdunVkfKWOPcUYRIlVppkN/wG3dU1UbNFChS7i1jTngEK4hiL9fzsch +992C5WLWHiWqXZi5xzP6wUCImQRS5omk8Ks9Br1ICp8Gk+2tRpHTbYZj535sa/Wi +nbbNMYNQKPWleEJG6T2auFLtNfDlsBs8O9gN3OoK4xXiQNoFrYrilNVGOz4Vg8bC +X3ERzYDMo1tW5bBJUCeeIzoW3sUIMr/rK/7UFx6HseSNfc5BJ1g66b+gmyXsqwu+ +1rjAbZTaq7OSpfumpsfGwUNWdYXNrRHIo63LoOoeJyeuBRfYpYLWuGNnUKJyFilj +dlBTscAghl5EE8LrYD9HUWnUzbu4G+sreJMUNDjE+iQO0m4oxJUI0J9PxJBoqwwd +8XS0lteOKVj9QuFuGPMMY3QEvGYRlDEklVM8Ud54WzYFt1YdOmEe+pynQ1AJC/Nm +5fGOHPTe/6f9wVR0n6IBWH/xCFIRWo4pihzYYsZCp7EP +-----END ENCRYPTED PRIVATE KEY----- diff --git a/certs/intermediate.csr b/certs/intermediate.csr new file mode 100644 index 0000000..c871cba --- /dev/null +++ b/certs/intermediate.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICkDCCAXgCAQAwSzELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYD +VQQHDAVQdXNhbjENMAsGA1UECgwEV1VEQzENMAsGA1UEAwwEV1VEQzCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ7cjbi03DI1iIH60j4vqb394TbBy85V +dWXvoV76rOHxNKd+qlW1kVJML/laeHayq5V62ORJuyQjWdCE/T7mdz8BW5xuRcFf +jh9we1q0ceCR4S2VN8PlhxRn1hQ2UtnmFq39X9bQzpcqvdscdEBKgzRPoFDCfs9T +e+cqAxaK0oYE5sQ/is2KeHuwbyiip/1OmKLAQvBsojFnXJGtCFDKE8KUEUN1Zcfy +l/mBiZcFgPcLzVjqoHGAsXgiGu1/vCvYvdj8tNU/Ns6izHK+dkYtiRHTMx8+Kaf2 +mXeeqX1IRrjehE9fH00xnirgdT6z7vNDoHzmLyS/btf7cLjv8Yv52gUCAwEAAaAA +MA0GCSqGSIb3DQEBCwUAA4IBAQAmh5NaHCxpjzYBuTediHEJdJk6nQnLi4dPpRvD +JrGmVHdxpaoKRyj2tIW5DGnfshs57oDgbXDPw8yE2tYG64BKPD0/xhhv8ztdJgfE +gjlKZMvFWpwdNDcXmgYQDg4EkQl4el5druIIVYPIgg+mgHOsOE3FZVM8liDWlr7N +dCUQHDR+vvtE9rJf8RIMHH1DufrObQYffE0YwMjg6GGJeQvOYn7VSkYILzYxIA/S +neY/y9YBogE034LFXlnhbo2/p5bY2qtZkNdKsC80xc8BjPdsnFcEdldgSypAfFRE +r/9kCe7ur9RO5HHr7nlm1oiLya+IMmqIVSAdjfFBBq4h8q5z +-----END CERTIFICATE REQUEST----- diff --git a/certs/intermediate.pem b/certs/intermediate.pem new file mode 100644 index 0000000..3c3bdae --- /dev/null +++ b/certs/intermediate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgUCFFw5T+2r/PXbAFYdJUeCO9NurTMhMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDE0NzIwWhcNMzUw +NDE0MDE0NzIwWjBLMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMQ0wCwYDVQQDDARXVURDMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAntyNuLTcMjWIgfrSPi+pvf3hNsHLzlV1 +Ze+hXvqs4fE0p36qVbWRUkwv+Vp4drKrlXrY5Em7JCNZ0IT9PuZ3PwFbnG5FwV+O +H3B7WrRx4JHhLZU3w+WHFGfWFDZS2eYWrf1f1tDOlyq92xx0QEqDNE+gUMJ+z1N7 +5yoDForShgTmxD+KzYp4e7BvKKKn/U6YosBC8GyiMWdcka0IUMoTwpQRQ3Vlx/KX ++YGJlwWA9wvNWOqgcYCxeCIa7X+8K9i92Py01T82zqLMcr52Ri2JEdMzHz4pp/aZ +d56pfUhGuN6ET18fTTGeKuB1PrPu80OgfOYvJL9u1/twuO/xi/naBQIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBAQAudZAxVxMh/aUeTj6WK5mpVpUINmAwt938FVSQ3nUC +mTafNjSNfu/Me+PvjCkmf9RNk5BSdHVwxseOBaU0WZnvLM056ayyQnSwWT53WwtZ +CSb3VHBbwaJqAiAnhQxCHHhWDK4rtIPrWZ/SCek2E5l29ccZHTuVnaFF3pG8YRs1 ++UXVWeCB0fIJwQdugtGkzVzgO1HW7tKyG1BqCP9q9FnfODia9d6jKAA5dScBPanG +yPNI8eq4Fpk4R6P1jM2jw9pCJ4CW4lpgGQD0KATJve+ooHJqL3Ri6xkljNymGfbP +UD8eyRxgUaKUloQY2UbafzO+cLn2uRiB4sfn/T8hybeW +-----END CERTIFICATE----- diff --git a/certs/intermediate.srl b/certs/intermediate.srl new file mode 100644 index 0000000..464bd5c --- /dev/null +++ b/certs/intermediate.srl @@ -0,0 +1 @@ +58564901B71F527CD57B7B33EB145A3BF84502C0 diff --git a/certs/mysql_certs/ca.pem b/certs/mysql_certs/ca.pem new file mode 100644 index 0000000..bfcc81c --- /dev/null +++ b/certs/mysql_certs/ca.pem @@ -0,0 +1,40 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgUCFFw5T+2r/PXbAFYdJUeCO9NurTMhMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDE0NzIwWhcNMzUw +NDE0MDE0NzIwWjBLMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMQ0wCwYDVQQDDARXVURDMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAntyNuLTcMjWIgfrSPi+pvf3hNsHLzlV1 +Ze+hXvqs4fE0p36qVbWRUkwv+Vp4drKrlXrY5Em7JCNZ0IT9PuZ3PwFbnG5FwV+O +H3B7WrRx4JHhLZU3w+WHFGfWFDZS2eYWrf1f1tDOlyq92xx0QEqDNE+gUMJ+z1N7 +5yoDForShgTmxD+KzYp4e7BvKKKn/U6YosBC8GyiMWdcka0IUMoTwpQRQ3Vlx/KX ++YGJlwWA9wvNWOqgcYCxeCIa7X+8K9i92Py01T82zqLMcr52Ri2JEdMzHz4pp/aZ +d56pfUhGuN6ET18fTTGeKuB1PrPu80OgfOYvJL9u1/twuO/xi/naBQIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBAQAudZAxVxMh/aUeTj6WK5mpVpUINmAwt938FVSQ3nUC +mTafNjSNfu/Me+PvjCkmf9RNk5BSdHVwxseOBaU0WZnvLM056ayyQnSwWT53WwtZ +CSb3VHBbwaJqAiAnhQxCHHhWDK4rtIPrWZ/SCek2E5l29ccZHTuVnaFF3pG8YRs1 ++UXVWeCB0fIJwQdugtGkzVzgO1HW7tKyG1BqCP9q9FnfODia9d6jKAA5dScBPanG +yPNI8eq4Fpk4R6P1jM2jw9pCJ4CW4lpgGQD0KATJve+ooHJqL3Ri6xkljNymGfbP +UD8eyRxgUaKUloQY2UbafzO+cLn2uRiB4sfn/T8hybeW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIUJQjfesPiPpsGhsWZH4oTggUWVD8wDQYJKoZIhvcNAQEL +BQAwSzELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYDVQQHDAVQdXNh +bjENMAsGA1UECgwEV1VEQzENMAsGA1UEAwwEV1VEQzAeFw0yNTA0MTYwMTQzMDda +Fw0zNTA0MTQwMTQzMDdaMEsxCzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEO +MAwGA1UEBwwFUHVzYW4xDTALBgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDczXj3rCSwS+cv2FXTRWxcLxuE +m0DY9RqQfUZ6XM24IuBpxeicqIcGs8lYSWmx9cRFnxDG5beV5BSBOt3MoR9Q5Caf +Ld4JWHEIXicDk78vQjTE7QB1xPkieU0s4wqh9W0rPKEYsJ8SEhFgMUq5g5EiaVqQ +PRaaEeK1C0Zn0e5NdwcMgJ6E6uwij9E1U95QAw6AFyvqoS+s/1TsEcAkN3oIXdlD +kYy7lO4uLZI4Dfh77kfAf3LxKN/qE6WE20wyzipwb7yXFVzcTm5WoLSwV4axWrYF +Bc67x3X5t6K985Y1f5UMfnKq+JRAl0wFC0SOhS8NXHFVNQCbXvwldfcUSwc/AgMB +AAGjUzBRMB0GA1UdDgQWBBRa5Q7LNg15ZZYZSMQoowHLNtJtezAfBgNVHSMEGDAW +gBRa5Q7LNg15ZZYZSMQoowHLNtJtezAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQDXLVSeVyhCuw4oJoRw/wPwQEeBScxmpkSQvMzl19QhFXkxZxUT +5vhkCJc3lp1Ifa405n5I1+WFVhWyF1cSTXYddxz/eLjW1p54VNPXm5nxKzwwyftE +Ps36G32D0kdkk9pRqnsQ2Dt5j8hf2RVXzyswu97mTkRXTQMDwKUtwopr6HlIZiOY +BzUOEJf4NQr3VeGkXeWfqjWMU7ch7FWf7SsyqNFinAgiDLaIdEYSRBi8xiRJpOn4 +BUbhAlS2L4uGPWGa7TZ8sf8/s4Mdh784NSFn+4D6pVa7Quq5e6OmBVK4J851zAic +7mQsPhJLXactGWdksrjBSMQS7E5R8LG+k0j3 +-----END CERTIFICATE----- diff --git a/certs/mysql_certs/server-cert.pem b/certs/mysql_certs/server-cert.pem new file mode 100644 index 0000000..efbe92d --- /dev/null +++ b/certs/mysql_certs/server-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJTCCAg0CFFhWSQG3H1J81Xt7M+sUWjv4RQK+MA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDIwMTA4WhcNMzUw +NDE0MDIwMTA4WjBTMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMRUwEwYDVQQDDAxkZXZsb2ctbXlzcWww +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDoOhJ5apnT8jlYjtVh7rgh +F2DKtabV+H1gEq6NxsrzLlfzfn1BqC4OCznCZArYcTPjLLwfNGVsRzsVtm/mWSCY +GKffPURUHW/TqnRHhFw0GXB63NK8dEdvzQib7S5esvLmT+u9VSNgfNQCvpw3fyQD +akaGk2GP+pKU1Snmp7stmjUDv8G3UIVePfhizXvMqI2LnfDgAUnQioonAoyksq5s +6GMrUoJZvjVeJutt6Kec8sguAdOb/yvMQ7BBbweAZqwkgGkVz0gQUzJH79BLPbsq +FbHzpBs5pBajk1yrg9bRJe0hPDcLwWXqczr5tSxLHv966PhK98j1XA+Acq77ljod +AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAJIUK9L5sCg9U09p7FbwQbLdVB7p45xT +8NKkOpDxEN+kFacNkbbkL3pD64T/3dQ4LWNiWV2WlBdqJoMIl1Rafcd+Oj6OqiTW +l9ICl/dhul+Z3MGXGdvgfc49vHO4SPc/ijMzowA7+3oiopOVIsid7wVCxZDeueVQ +HJQiqyVMC6vTZBGfoMzbUcOZn+MJV8Q9Fnh3kcx+clhAYiKsh52RKrDGhZrRzvoF +cLuOJlL5IcHYMBRxfoeYdoD200qG2TZb7GnIxb/vvzeLIGYwb80amuXkHaNuAqNY +rfFOMk4EAg3UChDquFDkD6nhP3zgPTiLULHSVbPz54sV7RHtzW1xUHo= +-----END CERTIFICATE----- diff --git a/certs/mysql_certs/server-full-chain.pem b/certs/mysql_certs/server-full-chain.pem new file mode 100644 index 0000000..2265fda --- /dev/null +++ b/certs/mysql_certs/server-full-chain.pem @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIDJTCCAg0CFFhWSQG3H1J81Xt7M+sUWjv4RQK+MA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDIwMTA4WhcNMzUw +NDE0MDIwMTA4WjBTMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMRUwEwYDVQQDDAxkZXZsb2ctbXlzcWww +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDoOhJ5apnT8jlYjtVh7rgh +F2DKtabV+H1gEq6NxsrzLlfzfn1BqC4OCznCZArYcTPjLLwfNGVsRzsVtm/mWSCY +GKffPURUHW/TqnRHhFw0GXB63NK8dEdvzQib7S5esvLmT+u9VSNgfNQCvpw3fyQD +akaGk2GP+pKU1Snmp7stmjUDv8G3UIVePfhizXvMqI2LnfDgAUnQioonAoyksq5s +6GMrUoJZvjVeJutt6Kec8sguAdOb/yvMQ7BBbweAZqwkgGkVz0gQUzJH79BLPbsq +FbHzpBs5pBajk1yrg9bRJe0hPDcLwWXqczr5tSxLHv966PhK98j1XA+Acq77ljod +AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAJIUK9L5sCg9U09p7FbwQbLdVB7p45xT +8NKkOpDxEN+kFacNkbbkL3pD64T/3dQ4LWNiWV2WlBdqJoMIl1Rafcd+Oj6OqiTW +l9ICl/dhul+Z3MGXGdvgfc49vHO4SPc/ijMzowA7+3oiopOVIsid7wVCxZDeueVQ +HJQiqyVMC6vTZBGfoMzbUcOZn+MJV8Q9Fnh3kcx+clhAYiKsh52RKrDGhZrRzvoF +cLuOJlL5IcHYMBRxfoeYdoD200qG2TZb7GnIxb/vvzeLIGYwb80amuXkHaNuAqNY +rfFOMk4EAg3UChDquFDkD6nhP3zgPTiLULHSVbPz54sV7RHtzW1xUHo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDHTCCAgUCFFw5T+2r/PXbAFYdJUeCO9NurTMhMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDE0NzIwWhcNMzUw +NDE0MDE0NzIwWjBLMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMQ0wCwYDVQQDDARXVURDMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAntyNuLTcMjWIgfrSPi+pvf3hNsHLzlV1 +Ze+hXvqs4fE0p36qVbWRUkwv+Vp4drKrlXrY5Em7JCNZ0IT9PuZ3PwFbnG5FwV+O +H3B7WrRx4JHhLZU3w+WHFGfWFDZS2eYWrf1f1tDOlyq92xx0QEqDNE+gUMJ+z1N7 +5yoDForShgTmxD+KzYp4e7BvKKKn/U6YosBC8GyiMWdcka0IUMoTwpQRQ3Vlx/KX ++YGJlwWA9wvNWOqgcYCxeCIa7X+8K9i92Py01T82zqLMcr52Ri2JEdMzHz4pp/aZ +d56pfUhGuN6ET18fTTGeKuB1PrPu80OgfOYvJL9u1/twuO/xi/naBQIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBAQAudZAxVxMh/aUeTj6WK5mpVpUINmAwt938FVSQ3nUC +mTafNjSNfu/Me+PvjCkmf9RNk5BSdHVwxseOBaU0WZnvLM056ayyQnSwWT53WwtZ +CSb3VHBbwaJqAiAnhQxCHHhWDK4rtIPrWZ/SCek2E5l29ccZHTuVnaFF3pG8YRs1 ++UXVWeCB0fIJwQdugtGkzVzgO1HW7tKyG1BqCP9q9FnfODia9d6jKAA5dScBPanG +yPNI8eq4Fpk4R6P1jM2jw9pCJ4CW4lpgGQD0KATJve+ooHJqL3Ri6xkljNymGfbP +UD8eyRxgUaKUloQY2UbafzO+cLn2uRiB4sfn/T8hybeW +-----END CERTIFICATE----- diff --git a/certs/mysql_certs/server-key-nopass.pem b/certs/mysql_certs/server-key-nopass.pem new file mode 100644 index 0000000..a9a8c8f --- /dev/null +++ b/certs/mysql_certs/server-key-nopass.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDoOhJ5apnT8jlY +jtVh7rghF2DKtabV+H1gEq6NxsrzLlfzfn1BqC4OCznCZArYcTPjLLwfNGVsRzsV +tm/mWSCYGKffPURUHW/TqnRHhFw0GXB63NK8dEdvzQib7S5esvLmT+u9VSNgfNQC +vpw3fyQDakaGk2GP+pKU1Snmp7stmjUDv8G3UIVePfhizXvMqI2LnfDgAUnQioon +Aoyksq5s6GMrUoJZvjVeJutt6Kec8sguAdOb/yvMQ7BBbweAZqwkgGkVz0gQUzJH +79BLPbsqFbHzpBs5pBajk1yrg9bRJe0hPDcLwWXqczr5tSxLHv966PhK98j1XA+A +cq77ljodAgMBAAECggEAED6zuSOgZCevJEdFnQaugb4ZasS/SXuLBbP42vkbjFxj +Eaj5CSVDJ39YiIUoDxEIMK2Z8VLgf20SMIglFseIVKtw3thx1bKDdpqXbMNREeHv +sOCOq1k1ABinMAUs97nr7PmNQ57XjTHeQNzln6o4sjZ2fYaEziYYp+mrdzOnj952 +D0QJkoDbTjh3gVqvHAQ0rA+eej1Ffw2SXZZEZrvdgTrkVsF3WwWqx0OcPpyFSm2n +ZEYKHlNSltEXHifcGYrUc5o17cJMzshMDePn7MVC21bxDazS8tUpLaRkqt/Lpv1e +JuAVH1WPlNfG9dGDTqdwVI9VMT2by/fRFrHziHC2UQKBgQD+VSIXHabG1NNUZyfy +WG58nQXIkMIBO/eXa/bdOv4z1iA7J1w9z/zoi0pn+dec4oiAyCutn44VMTf8/LPq +vrsJAjzkRNWMkUo0aLj5gxEPQ82veCgtzbKRe/YaYi+64//UQFOZpsoXxiKwiUnA +QOYLUcBQFQsCAb7Wk9dBSPerSQKBgQDpv9ZTUczvMoOIYUP1PhmVnyUuGNEgE/hN +i8wclKjEWUeNW4CpahB+KZZu8pYA0cJu0ztIABpC6DwlOZBoe0J9gFFWaikjGnX5 +JJkZslvEBFWrK2NSpsPeu89/ePZhLix7pYJmM2zo4XtMK/0hX0DpcIN+xeR0cXjP +sUZJhO2kNQKBgQCU2pTSPKuA0c1CKAHsSC+aRXi+E2NIv6VAfZMFlmJzSk6g8H9/ +Of0GyYdp5YN5Mei8nutZefn5k032hpxytuDW+/VRkKv/0oVAuU4R0tEoQwHeQhAa +BrsNhSTb+j1/P7RasK99TW4YjgF9m0yL9i/tzhIljLtdmFHuWqbwcdlq6QKBgQDp +KS5E1iexwZVqiHsdOeCTWrffj2mqscDQuU3UhIUDtnqlCk0AsIfbEOi5qsjt8E4d +9h3/5/pKGxVDnHPrhGgCf+iiZiq6lT5wUo1VEJBwqlI594GPhEGE/5ou8R3yOfit +LZ8xCsLsWV5/0LEihL1fHZhM8GC9tiJoKdCOrUXOsQKBgQDuaA2ZsV22PSNGdxRc +G/ofW1DUtW8gYHvykbC6RIWpYnRcXoFigSuSyZQQoAstQn4J1m/sZPgUFjqOZicx +QHgWKv4LAbGjI0GMNNEQdixI1XYubBUUIrv4vGFUsxo6OJxNQ1dzdzcoLjgrakvl +xZsIgVhJNixol+849LXUX1XF5Q== +-----END PRIVATE KEY----- diff --git a/certs/mysql_certs/server-key.pem b/certs/mysql_certs/server-key.pem new file mode 100644 index 0000000..c0589fc --- /dev/null +++ b/certs/mysql_certs/server-key.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQITSnU481rBeoCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBDxAJ8qnGwZyOjPHlycB5o2BIIE +0CyZYQ7m1WPMbJ+Pmq0C/wEC6VGJ+UghhzSArJIshRRB7iNmIat9GwKqyaA9tAP0 +eiSVv2YYLO8k3r7zqyFUAgY8HjX+PLbpWoy7OuR7kR3mzrbXNa4fyP9KgL/WU2mc +JrWlOzFSt0y283qADeHsEVPJNHwIPKrhpSaNLbDO9mfRJHSaNF5kXlxJWcN5iMEn +RaDc2vF5FIkshvql6Sad4HRjP74ThVzN7yqiQeVHZSZOUGs4hMkzZmJ9Uwq6zMHl +NxSkrz0Ig+47bpAtsmir1NGtRm3YuXS1wkrfmFvek18kLmmVPklxH1cW/2JbWrJB +SeohqITG2xECKKySQpTnbZCtTuTgVbg3nkn4qDf3xarUjs+0CPf17oiXPPImsec0 +opukyA/AVQzL/ujtoAxgEcpJvAoOSMwuwDfWcZCfPWSeOkSqZce1vFjBReTiNvE+ +yctQ8R8fbrXxLzPfKSZqfRWPYTrhNkPzPVdO7AzrWs2yr9p1x8qLdJdxc/VdBvJe ++Ywzb6qq6Ds36dCPVisWfieHyZ5jPC7x6h4WRbQi2Rxp3c0CU8bolLKB4IggdSn1 +nRTHQhzWpBpIpex+ARJjW56KxBOk/qDEsK+Pde7cayokyK99s9ri/Sovh9178koO +AVoVJa0XemihtYEca4JuQNEj9t27g3/ekBS6RVrnQeJhPluLwhtA0vRz36OJGPzY +yXbKfb0l63p9bJw4PD7yCIfUE6/4yW9aOBWUsYQU8ETgUXlH8WaGaSCQg1oGAOCH +M7mqLWme3YxR2NQNhbMefc+YBfp4VJcidp3wSqVPN7/VleZ/7TasCNtDoxZEklqZ +BkbK8coMK6sj+mnCODDlG8iae2Yg+UhuZrzDJZ8NTx6mMObzXuKatJQ/fn2tay09 +VIUp907gqM5eROdArfvHEAojQRW3+9759UwjM0fTN+1VRXRD9tV2jofx4Q0aK43G +L8Fh5gyN+A/c8exA4aoJmq/4eoMrEjrgnybKby9DS86B8PBC1MW26gjM1yHE82j1 +UoGeNH6a/qRg+XmJOQJ+JXte4o3JwPb0PB7M9KxF5uquuG6oN0gGDPTNej5imdIV +ZPPWX+ZxdGiwy2xsWbegf2VMTlQL7mHcHeW+k2fYNgs0zhPyZE/bkV9PWta/ZQ3s +Y+A1DxskICE+sPXIxQXugxdTgyOfA8Xuq5EJZszQ+AFoyOo27BA3wCIotXlGXfEy ++8xlvHcobKSa7+lQJH8MfExk7wQEMsPWx/j2P39aPXTEim7XGNPNTKYbFTI+2yuY +TjqkrCeLuG9vz9vm1JjPNmK/DM0lKc5QAmr7p+CPEezWkhyriMSpUOc6eLK77I44 +DxgDFss1tPeMaU/Nd1nxntmkWDYm91Lz69TYlk9r99cFvovYdihq/3lULzsJ5aw/ +13UFtm0/HLMeKYdvhfBQ/jx4XUdeQBUOY09xk9g82zVPOG7vaxKqj0VHW0EEF+K2 ++2okHVIKFVcbxhKOjYLs4cdtKhRWG1MO7BBZw6vyuOCmHk2/03v/j3+dH2IJ1pzV +r8qCzlslNKpHKHAaGQ7weKSpCRQydvXnu4w82lt7PQ59ErEXHpaFtulvPtMs77aR +5eTksWcbH7YdFnJ2bB5AEw13yEEVW56PSPNjYRAiuWCe +-----END ENCRYPTED PRIVATE KEY----- diff --git a/certs/mysql_certs/server.csr b/certs/mysql_certs/server.csr new file mode 100644 index 0000000..467e846 --- /dev/null +++ b/certs/mysql_certs/server.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICmDCCAYACAQAwUzELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYD +VQQHDAVQdXNhbjENMAsGA1UECgwEV1VEQzEVMBMGA1UEAwwMZGV2bG9nLW15c3Fs +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6DoSeWqZ0/I5WI7VYe64 +IRdgyrWm1fh9YBKujcbK8y5X8359QaguDgs5wmQK2HEz4yy8HzRlbEc7FbZv5lkg +mBin3z1EVB1v06p0R4RcNBlwetzSvHRHb80Im+0uXrLy5k/rvVUjYHzUAr6cN38k +A2pGhpNhj/qSlNUp5qe7LZo1A7/Bt1CFXj34Ys17zKiNi53w4AFJ0IqKJwKMpLKu +bOhjK1KCWb41XibrbeinnPLILgHTm/8rzEOwQW8HgGasJIBpFc9IEFMyR+/QSz27 +KhWx86QbOaQWo5Ncq4PW0SXtITw3C8Fl6nM6+bUsSx7/euj4SvfI9VwPgHKu+5Y6 +HQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAAHo704+gQMIjN69TdqxuP4iXRxo +HEoIUj/zkTMPgxTVW33vtQmoSlwRia0o8q8+Y26CQzjs1V8ne47I7446MSAcG453 +qyIyPjmeqrvPSoWCRyzA43BuGpkAcV/C8MxnR6nG7js9cISzYXObABmuas0Qy7FN +1J1MjlMGqwymiS0rQoGQzqMkY2mqmAvk/DyCNv1GPUyZOBH1IwddwDLWq9fMvorT +7GKF0rwniW4ItrDPBEkLSdoJST1OTR69EMHpBQ4szo0nMlSwAx7k7SHm6je5m1Al +PEPuXMFRkI/ueMdsu+VPS6Cy/kWEhbvZEDq6hLxrVwI7j85WeEri49+p6sE= +-----END CERTIFICATE REQUEST----- diff --git a/certs/nginx_certs/ca.pem b/certs/nginx_certs/ca.pem new file mode 100644 index 0000000..bfcc81c --- /dev/null +++ b/certs/nginx_certs/ca.pem @@ -0,0 +1,40 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgUCFFw5T+2r/PXbAFYdJUeCO9NurTMhMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDE0NzIwWhcNMzUw +NDE0MDE0NzIwWjBLMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMQ0wCwYDVQQDDARXVURDMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAntyNuLTcMjWIgfrSPi+pvf3hNsHLzlV1 +Ze+hXvqs4fE0p36qVbWRUkwv+Vp4drKrlXrY5Em7JCNZ0IT9PuZ3PwFbnG5FwV+O +H3B7WrRx4JHhLZU3w+WHFGfWFDZS2eYWrf1f1tDOlyq92xx0QEqDNE+gUMJ+z1N7 +5yoDForShgTmxD+KzYp4e7BvKKKn/U6YosBC8GyiMWdcka0IUMoTwpQRQ3Vlx/KX ++YGJlwWA9wvNWOqgcYCxeCIa7X+8K9i92Py01T82zqLMcr52Ri2JEdMzHz4pp/aZ +d56pfUhGuN6ET18fTTGeKuB1PrPu80OgfOYvJL9u1/twuO/xi/naBQIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBAQAudZAxVxMh/aUeTj6WK5mpVpUINmAwt938FVSQ3nUC +mTafNjSNfu/Me+PvjCkmf9RNk5BSdHVwxseOBaU0WZnvLM056ayyQnSwWT53WwtZ +CSb3VHBbwaJqAiAnhQxCHHhWDK4rtIPrWZ/SCek2E5l29ccZHTuVnaFF3pG8YRs1 ++UXVWeCB0fIJwQdugtGkzVzgO1HW7tKyG1BqCP9q9FnfODia9d6jKAA5dScBPanG +yPNI8eq4Fpk4R6P1jM2jw9pCJ4CW4lpgGQD0KATJve+ooHJqL3Ri6xkljNymGfbP +UD8eyRxgUaKUloQY2UbafzO+cLn2uRiB4sfn/T8hybeW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIUJQjfesPiPpsGhsWZH4oTggUWVD8wDQYJKoZIhvcNAQEL +BQAwSzELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYDVQQHDAVQdXNh +bjENMAsGA1UECgwEV1VEQzENMAsGA1UEAwwEV1VEQzAeFw0yNTA0MTYwMTQzMDda +Fw0zNTA0MTQwMTQzMDdaMEsxCzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEO +MAwGA1UEBwwFUHVzYW4xDTALBgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDczXj3rCSwS+cv2FXTRWxcLxuE +m0DY9RqQfUZ6XM24IuBpxeicqIcGs8lYSWmx9cRFnxDG5beV5BSBOt3MoR9Q5Caf +Ld4JWHEIXicDk78vQjTE7QB1xPkieU0s4wqh9W0rPKEYsJ8SEhFgMUq5g5EiaVqQ +PRaaEeK1C0Zn0e5NdwcMgJ6E6uwij9E1U95QAw6AFyvqoS+s/1TsEcAkN3oIXdlD +kYy7lO4uLZI4Dfh77kfAf3LxKN/qE6WE20wyzipwb7yXFVzcTm5WoLSwV4axWrYF +Bc67x3X5t6K985Y1f5UMfnKq+JRAl0wFC0SOhS8NXHFVNQCbXvwldfcUSwc/AgMB +AAGjUzBRMB0GA1UdDgQWBBRa5Q7LNg15ZZYZSMQoowHLNtJtezAfBgNVHSMEGDAW +gBRa5Q7LNg15ZZYZSMQoowHLNtJtezAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQDXLVSeVyhCuw4oJoRw/wPwQEeBScxmpkSQvMzl19QhFXkxZxUT +5vhkCJc3lp1Ifa405n5I1+WFVhWyF1cSTXYddxz/eLjW1p54VNPXm5nxKzwwyftE +Ps36G32D0kdkk9pRqnsQ2Dt5j8hf2RVXzyswu97mTkRXTQMDwKUtwopr6HlIZiOY +BzUOEJf4NQr3VeGkXeWfqjWMU7ch7FWf7SsyqNFinAgiDLaIdEYSRBi8xiRJpOn4 +BUbhAlS2L4uGPWGa7TZ8sf8/s4Mdh784NSFn+4D6pVa7Quq5e6OmBVK4J851zAic +7mQsPhJLXactGWdksrjBSMQS7E5R8LG+k0j3 +-----END CERTIFICATE----- diff --git a/certs/nginx_certs/client-ca.pem b/certs/nginx_certs/client-ca.pem new file mode 100644 index 0000000..bfcc81c --- /dev/null +++ b/certs/nginx_certs/client-ca.pem @@ -0,0 +1,40 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgUCFFw5T+2r/PXbAFYdJUeCO9NurTMhMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDE0NzIwWhcNMzUw +NDE0MDE0NzIwWjBLMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMQ0wCwYDVQQDDARXVURDMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAntyNuLTcMjWIgfrSPi+pvf3hNsHLzlV1 +Ze+hXvqs4fE0p36qVbWRUkwv+Vp4drKrlXrY5Em7JCNZ0IT9PuZ3PwFbnG5FwV+O +H3B7WrRx4JHhLZU3w+WHFGfWFDZS2eYWrf1f1tDOlyq92xx0QEqDNE+gUMJ+z1N7 +5yoDForShgTmxD+KzYp4e7BvKKKn/U6YosBC8GyiMWdcka0IUMoTwpQRQ3Vlx/KX ++YGJlwWA9wvNWOqgcYCxeCIa7X+8K9i92Py01T82zqLMcr52Ri2JEdMzHz4pp/aZ +d56pfUhGuN6ET18fTTGeKuB1PrPu80OgfOYvJL9u1/twuO/xi/naBQIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBAQAudZAxVxMh/aUeTj6WK5mpVpUINmAwt938FVSQ3nUC +mTafNjSNfu/Me+PvjCkmf9RNk5BSdHVwxseOBaU0WZnvLM056ayyQnSwWT53WwtZ +CSb3VHBbwaJqAiAnhQxCHHhWDK4rtIPrWZ/SCek2E5l29ccZHTuVnaFF3pG8YRs1 ++UXVWeCB0fIJwQdugtGkzVzgO1HW7tKyG1BqCP9q9FnfODia9d6jKAA5dScBPanG +yPNI8eq4Fpk4R6P1jM2jw9pCJ4CW4lpgGQD0KATJve+ooHJqL3Ri6xkljNymGfbP +UD8eyRxgUaKUloQY2UbafzO+cLn2uRiB4sfn/T8hybeW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIUJQjfesPiPpsGhsWZH4oTggUWVD8wDQYJKoZIhvcNAQEL +BQAwSzELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYDVQQHDAVQdXNh +bjENMAsGA1UECgwEV1VEQzENMAsGA1UEAwwEV1VEQzAeFw0yNTA0MTYwMTQzMDda +Fw0zNTA0MTQwMTQzMDdaMEsxCzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEO +MAwGA1UEBwwFUHVzYW4xDTALBgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDczXj3rCSwS+cv2FXTRWxcLxuE +m0DY9RqQfUZ6XM24IuBpxeicqIcGs8lYSWmx9cRFnxDG5beV5BSBOt3MoR9Q5Caf +Ld4JWHEIXicDk78vQjTE7QB1xPkieU0s4wqh9W0rPKEYsJ8SEhFgMUq5g5EiaVqQ +PRaaEeK1C0Zn0e5NdwcMgJ6E6uwij9E1U95QAw6AFyvqoS+s/1TsEcAkN3oIXdlD +kYy7lO4uLZI4Dfh77kfAf3LxKN/qE6WE20wyzipwb7yXFVzcTm5WoLSwV4axWrYF +Bc67x3X5t6K985Y1f5UMfnKq+JRAl0wFC0SOhS8NXHFVNQCbXvwldfcUSwc/AgMB +AAGjUzBRMB0GA1UdDgQWBBRa5Q7LNg15ZZYZSMQoowHLNtJtezAfBgNVHSMEGDAW +gBRa5Q7LNg15ZZYZSMQoowHLNtJtezAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQDXLVSeVyhCuw4oJoRw/wPwQEeBScxmpkSQvMzl19QhFXkxZxUT +5vhkCJc3lp1Ifa405n5I1+WFVhWyF1cSTXYddxz/eLjW1p54VNPXm5nxKzwwyftE +Ps36G32D0kdkk9pRqnsQ2Dt5j8hf2RVXzyswu97mTkRXTQMDwKUtwopr6HlIZiOY +BzUOEJf4NQr3VeGkXeWfqjWMU7ch7FWf7SsyqNFinAgiDLaIdEYSRBi8xiRJpOn4 +BUbhAlS2L4uGPWGa7TZ8sf8/s4Mdh784NSFn+4D6pVa7Quq5e6OmBVK4J851zAic +7mQsPhJLXactGWdksrjBSMQS7E5R8LG+k0j3 +-----END CERTIFICATE----- diff --git a/certs/nginx_certs/client-cert.pem b/certs/nginx_certs/client-cert.pem new file mode 100644 index 0000000..d0732d1 --- /dev/null +++ b/certs/nginx_certs/client-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJjCCAg4CFFw5T+2r/PXbAFYdJUeCO9NurTMkMA0GCSqGSIb3DQEBCwUAMEsx +CzAJBgNVBAYTAktSMQ4wDAYDVQQIDAVQdXNhbjEOMAwGA1UEBwwFUHVzYW4xDTAL +BgNVBAoMBFdVREMxDTALBgNVBAMMBFdVREMwHhcNMjUwNDE2MDYwMDM0WhcNMjYw +NDE2MDYwMDM0WjBUMQswCQYDVQQGEwJLUjEOMAwGA1UECAwFUHVzYW4xDjAMBgNV +BAcMBVB1c2FuMQ0wCwYDVQQKDARXVURDMRYwFAYDVQQDDA1kZXZsb2ctY2xpZW50 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3SbWErxx/vBb5icHdsGV +6H6UZ2yKHEth4QGaamc+xL56o15kW2rWE5WBltyvJloA1UcpEXmAcsqmAWj2YNPl +lZMDdq36PXX/cnDZON4+tIWseN1PZCS5ycxSayYrcmCnj2LDZzp7gLQKxlVsisqJ +GfaKrzjI7ysq/qWX3NaQa/OvayezUwyH68OLer4w1k7juh6bR3oWzbRBNFcnOzO2 +j9T5FITWN6fhjUPz9YXw71F7Mp0jdtDPxRZJTFIx3w+P/ikAeWTgLv6T7mEr8o3D +kXLKGY7ZFgv7BfvgqxDhoYbwGR4IfwljTME6hew51Gwp+vDUZ+frvikn8Q8ehUHV +OQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBx7hoHN/g6mKXxiZx4FBLvFt0BN1LO +YgP4s6bV3p979ar5FSHAFcOgyNa2zKMSNPCSkbEZUL+fzW9MrE+q2CcbaWG4nTGn +M6xWiuWXeTVASNpw6n+pq+fP4OlGjS7MrSjkCXyjhZu1MvhU4L3oJmH+bYEjsZxP +Jhgaq+BqySScFjkzh7xKqVVNwkNQD1bo0LuuE8Tpo6RFEblPsoa3KjQdby3HENhh +LqYzuxmcIevPLsW8UB8D2eIQ7mvHrViKrF5nIo7ioTvw8bp7bf7SrjGaHiQ/A8Ly +9iBkCGBx4cs0IrLvlcLvehbSwLKe4u+tH3BMOOljORS43xecI+RRsx9I +-----END CERTIFICATE----- diff --git a/certs/nginx_certs/client-key-nopass.pem b/certs/nginx_certs/client-key-nopass.pem new file mode 100644 index 0000000..7e614d7 --- /dev/null +++ b/certs/nginx_certs/client-key-nopass.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdJtYSvHH+8Fvm +Jwd2wZXofpRnbIocS2HhAZpqZz7EvnqjXmRbatYTlYGW3K8mWgDVRykReYByyqYB +aPZg0+WVkwN2rfo9df9ycNk43j60hax43U9kJLnJzFJrJityYKePYsNnOnuAtArG +VWyKyokZ9oqvOMjvKyr+pZfc1pBr869rJ7NTDIfrw4t6vjDWTuO6HptHehbNtEE0 +Vyc7M7aP1PkUhNY3p+GNQ/P1hfDvUXsynSN20M/FFklMUjHfD4/+KQB5ZOAu/pPu +YSvyjcORcsoZjtkWC/sF++CrEOGhhvAZHgh/CWNMwTqF7DnUbCn68NRn5+u+KSfx +Dx6FQdU5AgMBAAECggEAGo4mx4IsWBYJmvXVzhtJwmysqkycuvAMVUXpglxaa6qJ +tGNJvrZx9VEPCgv+1iaZkgKk+k2yMFaIH4Q4jYD0QQUxtccHVOj93wKQ+uSo9+lT +QAInHdnRG1u3C9m9/tJ/XFbaKuOZX+d+obkxOus+EkmJ9qdlbV6sH37H4QM8vVGA +8u8weVznVbguZsjbGPOT/vttc3vcbx0SVnVMrP1yBHM+ZQ0MSCsr8AdFhUv8rb7i +0lQuBhTRzSGYclfBdDRxOh1qVrR8qfZitdYj6feJzpIDMJA2LCmDu6nae6PtY/4H +bjJtXc3NvvED3QGoZQ7bapPxlANXB4Pw4zCCDt4yXwKBgQD14XjCMi5byCI+Qfae +N6bszeuDkZLcmX4b5btiWzbJPlStAQsL8bPrz6Fx5ZLYeEPuyys3EQUDuWRmY4Kr +2CkZaLb4itL85beZvR+/aXh5G8T2fNqZJkS8YxYVtYILPSh4wBB711hW5KsDclzT +iLyi0FHqJB1jdssP6GgoRFueZwKBgQDmQNOTDEvTqwWryh5BJypRDAAzWSXu0KKO +RCljC7zfuVSdEVz1BxrxyCKpNfH1MbpdiWQFeGR3nSzh2qtjew3oqrtr8Yksg683 +V8KtJ3XREWi9QtdkNmFH3oYEjhCJQ7X55v0iukspLAbVoU/SVtXh6bGyTBbQurCP +8AN8wWtrXwKBgHUBxeiL3rm4hGsiEsz56MqZt3CVztCBjpyR91j31RtxOPRXIb2e +WKNn3AkKWZX/rTwunLMIu10pVRjQU/eY1v4Lcb7WuU61tmhHsprxAu6HA3TUt2XX +6y/G61SLWoYkpWTI6U81jAlVqffq7TeQw0urXL/STdXuSvWYADDhTsQTAoGBAKO8 +Pbg36kQfPe0n0dPrEgCIVCwvnPXyj2YzumqgkjNWC4GWM1BbOSHufBdwMRt3vVt+ +tA3fyzH0J1KEuZQIkZ9+qcDdBfsNua/VTK7tfK6rfpv3yEuPECaXax4aGFBEQkfv +ptrnN0OT91g7WhPthDMeiCqOSTstRxlUSGaS9NxpAoGBAJ/UKTyFFR4Svke7ZkK+ +NuxYYkb/HImZcAmK05QvaWe2uEgseWqstiwtH2vviFaj+jcfZnoQLAPqbnxXzh5m +w0tnOiV7HGj1iOWCi/JmXVnp15tZbWDx7OJJHysLPMgDjcA5KRMjCm59h2Rv5s0g +c6G9Es1YnTU8RnJkWaIeQu0k +-----END PRIVATE KEY----- diff --git a/certs/nginx_certs/client-key.pem b/certs/nginx_certs/client-key.pem new file mode 100644 index 0000000..af92128 --- /dev/null +++ b/certs/nginx_certs/client-key.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQINAHMTU39tfkCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCzcw1fhM2NPxhjIR3HH22QBIIE +0M5TzHexIK8By+Riks9D+4sRSKCjCOKswrv4N23XbMUXGRKBIYVWV8hHYms15eh8 +NQApiTpsH1raa3ffsuO5tO/GPWqVpHXrPkQiDaq8liLb+TVBmm/nnLuXiogcwS5W +A6g7D0M9kwhxqHtxmQujKATIXm6eL2LZCI98y2wIexTtP9YJJqz8L4hOsH1bBIFQ +92HfvEHSkhMkjQq1YtALqL4fHdoOmJzyBEJeJIa22dfcNrS32sQpauK6m4+B4yaR +otTxJmLetrmb++lF8q6FwlevPsihod72aiN04tqLdEhPX2klm97NPFvVnMZ8Kx4d +hwgPwybGtwZBPc/bf8uehVhz8s8iqVbfhbbyBpuLaFbw0qFEuU+DtU7E4jfi+/qm +zLZd8919eNdicuOhQhv4ISvM47h9yzGcZaTp2P8dyvdp6BPbLWa22AuN9xDIlzWE +qmlpW847KEyy0YRksqx6HKlAinJ7xPz+ZIYsVu/ztMAK3UP6rwJ8G8lxObQQTdMf +Tpfg/SObjEvmEtB8z2daB0VFswrMf7i0lpIM4Ngksb4SkC3cMaLRUzOQzirOMQaw +XrHLzv3OBUAc+8i9WW2OQjHE5YDPDvQYIBStNKoE4XIr2B6jDlXy/TpzCpKXZkeD +6WnEAUFxgx5ZxpktTaL9NCfbKQralMXYdUannEsznE/R3Q/Vou0FqEJ/FVSpc79L +bGPZwIAlOtsASlmyxHTp58TpDRnaTjLx2ti4zArdOJjsErii/z+U52nAQnxy72Pw +RrcxrDwBmfAnRZ55vlTjTkygtOxUMd6rGPSfx84+M7ocXrJyn/ZhZC527/SDYDsF +vO8anqU5QHvg+GEhAlJk9yroXee4jpN3unBtt7NPQ4yDJfXgk95HvminNMJ9bUNw +MQ7r8suqXYVl8dS0raZ9zALOEFwtf2v/qFmWJrW9QM5AxvHoHgV0HzII2YHGMqek +OpZ9SzpexDxJcJgLKz0cohOxCg2AV8LFxV5m95pcc4/4SXXfNpbF2zcM/BuwRqDS +ZEvtOXcBwSCFMNA3mlnDtf5Bpdpazb5mdgUuQ8WuLGOJ+/bPRsL94oDTCzSPs1Ia +7HBCAJp7gK+/aj4iUVftLimr88I3uWmBcQrf8UIyKWocj0cIZGoAmbKRmgnriIRw +WRu6qXGWrEDsGiS3cAJjtCH3NgE2sIbomXGjqLigwl33n9Tws9bKC5M5JSb4OiS9 +l6dFZNFnMV86m+c3v3UVn0+xOvShT14eMradfuSUNHKVqeZC1q/nntuWrb1B6zQJ +fHCoPaSgQoafi7I9q7W6oZ42sN/fiwWvZTc8DJu0gAYrIUPnzCpmXxzoeB+RIeaz +kzTLAxiDql1jmZun9sthKPy15+hBJAfieol4XmC1jmfN1juJyGWCKpRLnifo6/Jl +k5HsQMn3CMadAwLY0uG+5pUH/6BR93pgAA5O7l2ee0gpcOjk9WEPmHhMzpGdX6j7 +MG4xh71g3rxDvYbSMp10df7OxtCnPmOFjR0SDAk+kzD/YUn4XzfOfd7Bxhe8/fhV +18mUbYMpNgGHaQ+K2Dl2Mu8KCQzIqUJ5pPjcBDW2BgbAS4noLcjgtu+eYaqZvx97 +BTYoQDaSwblPgoJDtQIL+hOtFPVVqSrRTGhxMne7Kh/Q +-----END ENCRYPTED PRIVATE KEY----- diff --git a/certs/nginx_certs/client.csr b/certs/nginx_certs/client.csr new file mode 100644 index 0000000..4774ba8 --- /dev/null +++ b/certs/nginx_certs/client.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICmTCCAYECAQAwVDELMAkGA1UEBhMCS1IxDjAMBgNVBAgMBVB1c2FuMQ4wDAYD +VQQHDAVQdXNhbjENMAsGA1UECgwEV1VEQzEWMBQGA1UEAwwNZGV2bG9nLWNsaWVu +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN0m1hK8cf7wW+YnB3bB +leh+lGdsihxLYeEBmmpnPsS+eqNeZFtq1hOVgZbcryZaANVHKRF5gHLKpgFo9mDT +5ZWTA3at+j11/3Jw2TjePrSFrHjdT2QkucnMUmsmK3Jgp49iw2c6e4C0CsZVbIrK +iRn2iq84yO8rKv6ll9zWkGvzr2sns1MMh+vDi3q+MNZO47oem0d6Fs20QTRXJzsz +to/U+RSE1jen4Y1D8/WF8O9RezKdI3bQz8UWSUxSMd8Pj/4pAHlk4C7+k+5hK/KN +w5FyyhmO2RYL+wX74KsQ4aGG8BkeCH8JY0zBOoXsOdRsKfrw1Gfn674pJ/EPHoVB +1TkCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQC1I9QFhPVVSKIRK/a/6sGQ/OPC +9aL+Ucv0XB93eZij8EnxVKuX+JU0LRcgZCw1J+AecBbl/kcmLtJcC3HbAxNxV9JV +lcKmki2tuaHpkiJ3AHGI1jpr6sAMyd9lhTtpDe+jq5jKreDauDH+vyBMuSNmpg70 +vHd7/O2ogBDwh6VkR+8JH8L7lztWFgddFLNqtjZUv6LErKREz9YiMCDJ/wA6WSj0 +2stXGIBx9QT4EKh7E4cTc/5BDLjrynFg2Xk//13iNnB3c4afmLEnCBChLsib+Q7F +LEmv79AR6ErBS0IYO/EMGH7g4eNkGosgNL6loNds4tbqslWaPGTkOOVat1+U +-----END CERTIFICATE REQUEST----- diff --git a/certs/nginx_certs/dhparam.pem b/certs/nginx_certs/dhparam.pem new file mode 100644 index 0000000..d7c1ead --- /dev/null +++ b/certs/nginx_certs/dhparam.pem @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEAj5Q42ch3w5QnqSRs0Lwx0qvYNwRHmDrxb8yAxkmFoPGYz4L2ykBz +q5iiJsskr0HWdWfav/zcgc8taEh8HRlWcYQqxZwx+X43J0Vxd8IaUBFJq8k8e6gs +5aa6NNhG2WJ28+xTyzMmgyxuadxU+CUmPJdQ6cZ2aJ1Pz46xpYyiip+UcIjlqve7 +Ok2FWlf6FendLL8MqKmaYatFbfwmuOYZ12rJiCPeFie41BTa1rZe/2LoM20+18zC +heS1stQjHLvUsNzIYIx/ecsLUf0MhMXPd/H4m4rPLkdkfyWeHZI56dt7/A5EQV2l +mf/jxmhGAW5cVi5Rq/YQ2yhXpN2xiGlXRwIBAg== +-----END DH PARAMETERS----- diff --git a/certs/nginx_certs/fullchain.pem b/certs/nginx_certs/fullchain.pem new file mode 100644 index 0000000..2962fab --- /dev/null +++ b/certs/nginx_certs/fullchain.pem @@ -0,0 +1,48 @@ +-----BEGIN CERTIFICATE----- +MIIDojCCAyigAwIBAgISBtX8xdMcA7U76qJ3ggfM6qwAMAoGCCqGSM49BAMDMDIx +CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF +NjAeFw0yNTA0MTQxODA0MDNaFw0yNTA3MTMxODA0MDJaMBQxEjAQBgNVBAMTCXd1 +ZGMubGluazBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABP5fTZQrB2xvspcZw6OE +E8BQRV10PVc+ES6LJyqV2Lm5vBDTW7p5Jrkux/v7rHlfRlLdA4+weAiyOiw9t1yw +W+qjggI6MIICNjAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEG +CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFNXtO0IsxQYmeK5lWd7E +VstPzIYBMB8GA1UdIwQYMBaAFJMnRpgDqVFojpjWxEJI2yO/WJTSMFUGCCsGAQUF +BwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL2U2Lm8ubGVuY3Iub3JnMCIGCCsG +AQUFBzAChhZodHRwOi8vZTYuaS5sZW5jci5vcmcvMBQGA1UdEQQNMAuCCXd1ZGMu +bGluazATBgNVHSAEDDAKMAgGBmeBDAECATAsBgNVHR8EJTAjMCGgH6AdhhtodHRw +Oi8vZTYuYy5sZW5jci5vcmcvMS5jcmwwggEFBgorBgEEAdZ5AgQCBIH2BIHzAPEA +dgDd3Mo0ldfhFgXnlTL6x5/4PRxQ39sAOhQSdgosrLvIKgAAAZY1ro31AAAEAwBH +MEUCIQCLqDa0QSg4SbMqvWqeJgMFQde83vn+5FymNcGzv/skwgIgPnLCaA+l0NXj +HKY6tZwxJsafvKwsX0IJkfKnM3iWZ6UAdwB9WR4S4XgqexxhZ3xe/fjQh1wUoE6V +nrkDL9kOjC55uAAAAZY1rpWPAAAEAwBIMEYCIQDSGl8VvseVkuOXdEa8vXARvtup +TiLy2X9i0vprMF2w0gIhAJc0nY/t5jyZ6l12tpOeWxnoaS2y6QlYR1NQxllMwheH +MAoGCCqGSM49BAMDA2gAMGUCME5DUISzz+IaJ7gGVl7ArksQ2U107kmcNCrAke5i +BlhD65cfHGB+iQR6JroepHvmnQIxAMv78mix461p4WzHrm6c6JSpqqzL0d9TTeyi +VOTSvJXRjK1SA1tMim9rpXRFobBsIg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEVzCCAj+gAwIBAgIRALBXPpFzlydw27SHyzpFKzgwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw +WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDELMAkGA1UEAxMCRTYwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATZ8Z5G +h/ghcWCoJuuj+rnq2h25EqfUJtlRFLFhfHWWvyILOR/VvtEKRqotPEoJhC6+QJVV +6RlAN2Z17TJOdwRJ+HB7wxjnzvdxEP6sdNgA1O1tHHMWMxCcOrLqbGL0vbijgfgw +gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD +ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSTJ0aYA6lRaI6Y1sRCSNsj +v1iU0jAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB +AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g +BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu +Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAfYt7SiA1sgWGCIpunk46r4AExIRc +MxkKgUhNlrrv1B21hOaXN/5miE+LOTbrcmU/M9yvC6MVY730GNFoL8IhJ8j8vrOL +pMY22OP6baS1k9YMrtDTlwJHoGby04ThTUeBDksS9RiuHvicZqBedQdIF65pZuhp +eDcGBcLiYasQr/EO5gxxtLyTmgsHSOVSBcFOn9lgv7LECPq9i7mfH3mpxgrRKSxH +pOoZ0KXMcB+hHuvlklHntvcI0mMMQ0mhYj6qtMFStkF1RpCG3IPdIwpVCQqu8GV7 +s8ubknRzs+3C/Bm19RFOoiPpDkwvyNfvmQ14XkyqqKK5oZ8zhD32kFRQkxa8uZSu +h4aTImFxknu39waBxIRXE4jKxlAmQc4QjFZoq1KmQqQg0J/1JF8RlFvJas1VcjLv +YlvUB2t6npO6oQjB3l+PNf0DpQH7iUx3Wz5AjQCi6L25FjyE06q6BZ/QlmtYdl/8 +ZYao4SRqPEs/6cAiF+Qf5zg2UkaWtDphl1LKMuTNLotvsX99HP69V2faNyegodQ0 +LyTApr/vT01YPE46vNsDLgK+4cL6TrzC/a4WcmF5SRJ938zrv/duJHLXQIku5v0+ +EwOy59Hdm0PT/Er/84dDV0CSjdR/2XuZM3kpysSKLgD1cKiDA+IRguODCxfO9cyY +Ig46v9mFmBvyH04= +-----END CERTIFICATE----- diff --git a/certs/nginx_certs/keystore.p12 b/certs/nginx_certs/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..15bf2f2990123eacdd2fc0fc1345a6e526fc690e GIT binary patch literal 3532 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 send(@Valid @RequestBody EmailSendRequestDto request) { + emailVerificationService.sendCode(request.getEmail()); + return CommonResponse.ok(null); + } + + @PostMapping("/verify") + @ResponseStatus(HttpStatus.OK) + public CommonResponse verify(@Valid @RequestBody EmailVerifyRequestDto request) { + return CommonResponse.ok(emailVerificationService.verifyCode(request.getEmail(), request.getCode())); + } +} diff --git a/src/main/java/apptive/devlog/domain/auth/dto/EmailSendRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/EmailSendRequestDto.java new file mode 100644 index 0000000..46800c1 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/dto/EmailSendRequestDto.java @@ -0,0 +1,14 @@ +package apptive.devlog.domain.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class EmailSendRequestDto { + @Email + @NotBlank + private String email; +} diff --git a/src/main/java/apptive/devlog/domain/auth/dto/EmailVerifyRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/EmailVerifyRequestDto.java new file mode 100644 index 0000000..316733f --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/dto/EmailVerifyRequestDto.java @@ -0,0 +1,18 @@ +package apptive.devlog.domain.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class EmailVerifyRequestDto { + @Email + @NotBlank + private String email; + + @NotBlank + private String code; +} \ No newline at end of file diff --git a/src/main/java/apptive/devlog/domain/auth/dto/EmailVerifyResponseDto.java b/src/main/java/apptive/devlog/domain/auth/dto/EmailVerifyResponseDto.java new file mode 100644 index 0000000..a012f8d --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/dto/EmailVerifyResponseDto.java @@ -0,0 +1,13 @@ +package apptive.devlog.domain.auth.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Schema(description = "이메일 인증 응답 DTO") +public class EmailVerifyResponseDto { + @Schema(description = "이메일 인증 토큰", example = "eyJhbGciOiJIUzI1...") + private String token; +} diff --git a/src/main/java/apptive/devlog/domain/auth/dto/UserSignupRequestDto.java b/src/main/java/apptive/devlog/domain/auth/dto/UserSignupRequestDto.java index f509029..82ed07f 100644 --- a/src/main/java/apptive/devlog/domain/auth/dto/UserSignupRequestDto.java +++ b/src/main/java/apptive/devlog/domain/auth/dto/UserSignupRequestDto.java @@ -22,16 +22,20 @@ @AllArgsConstructor @Schema(description = "회원가입 요청 DTO") public class UserSignupRequestDto { - @Schema(description = "이메일", example = "test@example.com", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "이메일", example = "user@example.com", requiredMode = Schema.RequiredMode.REQUIRED) @Email @NotBlank private String email; - @Schema(description = "비밀번호 (8자 이상 20자 이하)", example = "P@ssword123", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "비밀번호 (8자 이상 20자 이하)", example = "securePassword123!", requiredMode = Schema.RequiredMode.REQUIRED) @Size(min = 8, max = 20) @NotBlank private String password; + @Schema(description = "이메일 인증 토큰", example = "", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank + private String token; + @Schema(description = "이름", example = "홍길동", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank private String name; diff --git a/src/main/java/apptive/devlog/domain/auth/repository/VerifiedEmailRepository.java b/src/main/java/apptive/devlog/domain/auth/repository/VerifiedEmailRepository.java new file mode 100644 index 0000000..3d813e8 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/repository/VerifiedEmailRepository.java @@ -0,0 +1,48 @@ +package apptive.devlog.domain.auth.repository; + +import apptive.devlog.common.response.error.exception.InvalidEmailVerificationTokenException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +@Repository +@RequiredArgsConstructor +public class VerifiedEmailRepository { + private static final String CODE_EMAIL_PREFIX = "CE:"; + private static final String VERIFY_EMAIL_PREFIX = "VE:"; + private final Duration EXPIRATION = Duration.ofMinutes(3); + private final RedisTemplate redisTemplate; + + public void saveCode(String email, String code) { + redisTemplate.opsForValue().set(CODE_EMAIL_PREFIX + email, code, EXPIRATION); + } + + public Optional getCode(String email) { + return Optional.ofNullable(redisTemplate.opsForValue().get(CODE_EMAIL_PREFIX + email)); + } + + public void deleteCode(String email) { + redisTemplate.delete(CODE_EMAIL_PREFIX + email); + } + + public String markVerified(String email) { + String token = UUID.randomUUID().toString(); + redisTemplate.opsForValue().set(VERIFY_EMAIL_PREFIX + email, token, EXPIRATION); + return token; + } + + public boolean isVerified(String email, String token) { + return Boolean.TRUE.equals(Optional.ofNullable(redisTemplate.opsForValue().get(VERIFY_EMAIL_PREFIX + email)) + .map(storedToken -> Objects.equals(token, storedToken)) + .orElseThrow(InvalidEmailVerificationTokenException::new)); + } + + public void deleteVerified(String email) { + redisTemplate.delete(VERIFY_EMAIL_PREFIX + email); + } +} diff --git a/src/main/java/apptive/devlog/domain/auth/service/AuthService.java b/src/main/java/apptive/devlog/domain/auth/service/AuthService.java index 0681b92..324d186 100644 --- a/src/main/java/apptive/devlog/domain/auth/service/AuthService.java +++ b/src/main/java/apptive/devlog/domain/auth/service/AuthService.java @@ -21,12 +21,17 @@ public class AuthService { private final PasswordEncoder passwordEncoder; private final JwtTokenProvider jwtTokenProvider; private final RedisRepository redisRepository; + private final EmailVerificationService emailVerificationService; @Transactional public UserSignupResponseDto signup(UserSignupRequestDto requestDto) { if (userRepository.existsByEmail(requestDto.getEmail())) { throw new DuplicateEmailException(); } + if (!emailVerificationService.isVerified(requestDto.getEmail(), requestDto.getToken())) { + throw new IllegalArgumentException("이메일 인증이 필요합니다."); + } + User user = requestDto.toEntity(passwordEncoder); userRepository.save(user); return new UserSignupResponseDto(user); diff --git a/src/main/java/apptive/devlog/domain/auth/service/EmailVerificationService.java b/src/main/java/apptive/devlog/domain/auth/service/EmailVerificationService.java new file mode 100644 index 0000000..291303f --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/service/EmailVerificationService.java @@ -0,0 +1,38 @@ +package apptive.devlog.domain.auth.service; + +import apptive.devlog.domain.auth.dto.EmailVerifyResponseDto; +import apptive.devlog.domain.auth.repository.VerifiedEmailRepository; +import apptive.devlog.domain.auth.util.MailSender; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Random; + +@Service +@RequiredArgsConstructor +public class EmailVerificationService { + private final MailSender mailSender; + private final VerifiedEmailRepository verifiedEmailRepository; + + public void sendCode(String email) { + String code = generateCode(); + verifiedEmailRepository.saveCode(email, code); + mailSender.sendEmail(email, "[Devlog] 인증 코드", "인증 코드: " + code); + } + + public EmailVerifyResponseDto verifyCode(String email, String code) { + String storedCode = verifiedEmailRepository.getCode(email) + .orElseThrow(() -> new IllegalArgumentException("코드가 만료되었거나 존재하지 않습니다.")); + if (!storedCode.equals(code)) throw new IllegalArgumentException("코드가 일치하지 않습니다."); + verifiedEmailRepository.deleteCode(email); + return new EmailVerifyResponseDto(verifiedEmailRepository.markVerified(email)); + } + + public boolean isVerified(String email, String token) { + return verifiedEmailRepository.isVerified(email, token); + } + + private String generateCode() { + return String.format("%06d", new Random().nextInt(1_000_000)); + } +} diff --git a/src/main/java/apptive/devlog/domain/auth/util/MailSender.java b/src/main/java/apptive/devlog/domain/auth/util/MailSender.java new file mode 100644 index 0000000..1cc1dc3 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/util/MailSender.java @@ -0,0 +1,41 @@ +package apptive.devlog.domain.auth.util; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.MailSendException; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +@Component +@RequiredArgsConstructor +public class MailSender { + private final JavaMailSender javaMailSender; + @Value("${spring.mail.username}") + private String fromAddress; + + public void sendEmail(String to, String subject, String text) { + try { + MimeMessage message = javaMailSender.createMimeMessage(); + // multipart=false, encoding=UTF-8 + MimeMessageHelper helper = new MimeMessageHelper( + message, + /* multipart */ false, + StandardCharsets.UTF_8.name() + ); + helper.setFrom(fromAddress); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(text, /* isHtml */ false); + + javaMailSender.send(message); + } catch (MessagingException e) { + throw new MailSendException("메일 전송 실패", e); + } + } +} + diff --git a/src/main/java/apptive/devlog/domain/auth/util/RandomCodeGenerator.java b/src/main/java/apptive/devlog/domain/auth/util/RandomCodeGenerator.java new file mode 100644 index 0000000..3b4f962 --- /dev/null +++ b/src/main/java/apptive/devlog/domain/auth/util/RandomCodeGenerator.java @@ -0,0 +1,11 @@ +package apptive.devlog.domain.auth.util; + +import java.util.Random; + +public class RandomCodeGenerator { + public static String generate6DigitCode() { + Random random = new Random(); + int code = 100000 + random.nextInt(900000); + return String.valueOf(code); + } +} diff --git a/src/main/java/apptive/devlog/domain/post/dto/PostDto.java b/src/main/java/apptive/devlog/domain/post/dto/PostDto.java index 3ce9a77..d8dacde 100644 --- a/src/main/java/apptive/devlog/domain/post/dto/PostDto.java +++ b/src/main/java/apptive/devlog/domain/post/dto/PostDto.java @@ -1,6 +1,5 @@ package apptive.devlog.domain.post.dto; -import apptive.devlog.domain.user.entity.User; import jakarta.validation.constraints.NotBlank; import lombok.Builder; diff --git a/src/main/java/apptive/devlog/domain/post/entity/Post.java b/src/main/java/apptive/devlog/domain/post/entity/Post.java index 1ac0809..c6d28c6 100644 --- a/src/main/java/apptive/devlog/domain/post/entity/Post.java +++ b/src/main/java/apptive/devlog/domain/post/entity/Post.java @@ -29,6 +29,7 @@ public class Post extends BaseEntity { @JoinColumn(name = "author_id", nullable = false) private User author; + @Builder.Default @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) private List comments = new ArrayList<>(); diff --git a/src/main/java/apptive/devlog/global/config/MailConfig.java b/src/main/java/apptive/devlog/global/config/MailConfig.java new file mode 100644 index 0000000..22dd913 --- /dev/null +++ b/src/main/java/apptive/devlog/global/config/MailConfig.java @@ -0,0 +1,40 @@ +package apptive.devlog.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class MailConfig { + + @Bean + public JavaMailSender javaMailSender( + @Value("${spring.mail.host}") String host, + @Value("${spring.mail.port}") int port, + @Value("${spring.mail.username}") String username, + @Value("${spring.mail.password}") String password + ) { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(host); + mailSender.setPort(port); + mailSender.setUsername(username); + mailSender.setPassword(password); + + Properties props = mailSender.getJavaMailProperties(); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.debug", "false"); + + // ↓ 추가: UTF-8 헤더·본문 인코딩 허용 + props.put("mail.mime.charset", "UTF-8"); + props.put("mail.mime.allowutf8", "true"); + props.put("mail.mime.allowutf8header", "true"); + props.put("mail.smtp.allow8bitmime", "true"); + + return mailSender; + } +} diff --git a/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java b/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java index 26c984d..00d0361 100644 --- a/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java +++ b/src/main/java/apptive/devlog/global/security/config/SecurityConfig.java @@ -58,7 +58,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .csrf(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - .requestMatchers("/", "/auth/signup", "/auth/login", "/auth/refresh", "/oauth2/**").permitAll() + .requestMatchers("/", "/auth/signup", "/auth/login", "/auth/refresh", "/oauth2/**", "/auth/email/*").permitAll() .requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/v3/api-docs", "/v3/api-docs.yaml", "/swagger-resources/**", "/webjars/**", "/api-docs/**").permitAll() .requestMatchers("/auth/logout", "/user/profile").hasRole("USER") .requestMatchers("/post/**", "/comment/**").hasRole("USER") diff --git a/src/main/java/apptive/devlog/infrastructure/redis/config/RedisConfig.java b/src/main/java/apptive/devlog/infrastructure/redis/config/RedisConfig.java deleted file mode 100644 index 3421510..0000000 --- a/src/main/java/apptive/devlog/infrastructure/redis/config/RedisConfig.java +++ /dev/null @@ -1,39 +0,0 @@ -package apptive.devlog.infrastructure.redis.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -@Configuration -public class RedisConfig { - @Value("${spring.data.redis.host}") - private String redisHost; - - @Value("${spring.data.redis.port}") - private int redisPort; - - @Bean - public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(redisHost, redisPort); - } - - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - StringRedisSerializer stringSerializer = new StringRedisSerializer(); - - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(redisConnectionFactory); - - redisTemplate.setKeySerializer(stringSerializer); - redisTemplate.setValueSerializer(stringSerializer); - redisTemplate.setHashKeySerializer(stringSerializer); - redisTemplate.setHashValueSerializer(stringSerializer); - - redisTemplate.afterPropertiesSet(); - return redisTemplate; - } -} diff --git a/src/main/java/apptive/devlog/infrastructure/redis/config/RedisJedisMutualTlsConfig.java b/src/main/java/apptive/devlog/infrastructure/redis/config/RedisJedisMutualTlsConfig.java new file mode 100644 index 0000000..ef8f9aa --- /dev/null +++ b/src/main/java/apptive/devlog/infrastructure/redis/config/RedisJedisMutualTlsConfig.java @@ -0,0 +1,94 @@ +package apptive.devlog.infrastructure.redis.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.data.redis.connection.RedisPassword; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import javax.net.ssl.*; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.KeyStore; + +@Configuration +@Profile("prod") +public class RedisJedisMutualTlsConfig { + + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + + @Value("${spring.data.redis.password}") + private String redisPassword; + + @Value("${spring.redis.ssl.keystore-path}") + private String keyStorePath; + @Value("${spring.redis.ssl.keystore-password}") + private String keyStorePassword; + @Value("${spring.redis.ssl.keystore-type}") + private String keyStoreType; + + @Value("${spring.redis.ssl.truststore-path}") + private String trustStorePath; + @Value("${spring.redis.ssl.truststore-password}") + private String trustStorePassword; + @Value("${spring.redis.ssl.truststore-type}") + private String trustStoreType; + + @Bean + public JedisConnectionFactory jedisConnectionFactory() throws Exception { + + // --- SSLContext 생성 (keystore + truststore 로 mutual TLS) --- + char[] keyPass = keyStorePassword.toCharArray(); + KeyStore ks = KeyStore.getInstance(keyStoreType); + try (InputStream ksStream = new FileInputStream(keyStorePath)) { + ks.load(ksStream, keyPass); + } + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, keyPass); + + char[] trustPass = trustStorePassword.toCharArray(); + KeyStore ts = KeyStore.getInstance(trustStoreType); + try (InputStream tsStream = new FileInputStream(trustStorePath)) { + ts.load(tsStream, trustPass); + } + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ts); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + // --- JedisClientConfiguration 설정 --- + JedisClientConfiguration jedisClientConfig = JedisClientConfiguration.builder() + .useSsl() + .sslSocketFactory(sslContext.getSocketFactory()) + .build(); + + // --- RedisStandaloneConfiguration 설정 --- + RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration(redisHost, redisPort); + serverConfig.setPassword(RedisPassword.of(redisPassword)); + + return new JedisConnectionFactory(serverConfig, jedisClientConfig); + } + + @Bean + public RedisTemplate redisTemplate(JedisConnectionFactory cf) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(cf); + StringRedisSerializer s = new StringRedisSerializer(); + template.setKeySerializer(s); + template.setValueSerializer(s); + template.setHashKeySerializer(s); + template.setHashValueSerializer(s); + template.afterPropertiesSet(); + return template; + } +} diff --git a/src/main/java/apptive/devlog/infrastructure/redis/config/RedisLocalConfig.java b/src/main/java/apptive/devlog/infrastructure/redis/config/RedisLocalConfig.java new file mode 100644 index 0000000..e3f551d --- /dev/null +++ b/src/main/java/apptive/devlog/infrastructure/redis/config/RedisLocalConfig.java @@ -0,0 +1,40 @@ +package apptive.devlog.infrastructure.redis.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@Profile("local") +public class RedisLocalConfig { + + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + + @Bean + public JedisConnectionFactory jedisConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort); + return new JedisConnectionFactory(config); + } + + @Bean + public RedisTemplate redisTemplate(JedisConnectionFactory cf) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(cf); + StringRedisSerializer s = new StringRedisSerializer(); + template.setKeySerializer(s); + template.setValueSerializer(s); + template.setHashKeySerializer(s); + template.setHashValueSerializer(s); + template.afterPropertiesSet(); + return template; + } +} diff --git a/src/main/resources/application-release.properties b/src/main/resources/application-dev.properties similarity index 57% rename from src/main/resources/application-release.properties rename to src/main/resources/application-dev.properties index 4e5ab82..bc3dbf9 100644 --- a/src/main/resources/application-release.properties +++ b/src/main/resources/application-dev.properties @@ -1,13 +1,54 @@ # =============================== # DB CONFIG # =============================== -spring.datasource.url=jdbc:mysql://172.23.171.122:3307/devlog?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true +spring.datasource.url=jdbc:mysql://devlog-mysql:3306/devlog?allowPublicKeyRetrieval=true&useSSL=true&verifyServerCertificate=true&requireSSL=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&sslMode=VERIFY_CA&sslCa=/certs/ca.pem&sslCert=/certs/server-cert.pem&sslKey=/certs/server-key.pem spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.username=root +spring.datasource.hikari.data-source-properties.useSSL=true +spring.datasource.hikari.data-source-properties.requireSSL=true +spring.datasource.hikari.data-source-properties.trustCertificateKeyStoreUrl=file:/certs/truststore.p12 +spring.datasource.hikari.data-source-properties.trustCertificateKeyStoreType=PKCS12 +spring.datasource.hikari.data-source-properties.keyStore=file:/certs/keystore.p12 +spring.datasource.hikari.data-source-properties.keyStoreType=PKCS12 +spring.datasource.hikari.data-source-properties.keyAlias=devlog-app +spring.datasource.hikari.data-source-properties.clientCertificateKeyStoreUrl=file:/certs/keystore.p12 +spring.datasource.hikari.data-source-properties.clientCertificateKeyStoreType=PKCS12 + +spring.datasource.hikari.connection-timeout=30000 +spring.datasource.hikari.maximum-pool-size=10 + +# =============================== +# REDIS CONFIG +# =============================== +spring.data.redis.host=devlog-redis +spring.data.redis.port=6380 +spring.data.redis.timeout=5s +spring.data.redis.ssl.enabled=true +spring.data.redis.url=rediss://devlog-redis:6380 +spring.data.redis.client-type=jedis +spring.redis.ssl.keystore-path=/certs/keystore.p12 +spring.redis.ssl.truststore-path=/certs/truststore.p12 +spring.redis.ssl.keystore-type=PKCS12 +spring.redis.ssl.truststore-type=PKCS12s + +# =============================== +# SERVER CONFIG +# =============================== +server.address=0.0.0.0 +server.port=443 +server.ssl.enabled=true +server.ssl.key-alias=devlog-app +server.ssl.key-store=/certs/keystore.p12 +server.ssl.key-store-type=PKCS12 +server.ssl.client-auth=need +server.ssl.trust-store=/certs/truststore.p12 +server.ssl.trust-store-type=PKCS12 + # =============================== # JPA CONFIG # =============================== +logging.level.org.springframework.boot.web.embedded.tomcat.SslConnectorCustomizer=DEBUG spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect spring.jpa.hibernate.ddl-auto=update @@ -16,29 +57,27 @@ spring.jpa.properties.hibernate.show_sql=false spring.jpa.open-in-view=false spring.jpa.show-sql=false -# =============================== -# SERVER CONFIG -# =============================== -server.port=8080 -server.ssl.enabled=false - -# =============================== -# REDIS CONFIG -# =============================== -spring.data.redis.host=devlog-redis -spring.data.redis.port=6379 - # =============================== # LOGGING # =============================== -logging.level.org.springframework.security=INFO -logging.level.com.apptive.devlog=INFO +logging.level.org.springframework.boot.context.config=DEBUG +logging.level.org.springframework.security=DEBUG +logging.level.com.apptive.devlog=DEBUG +logging.level.io.netty=DEBUG +logging.level.root=DEBUG +logging.level.org.springframework.web=DEBUG +logging.level.org.springframework.web.client.RestTemplate=DEBUG +logging.level.org.springframework.http=DEBUG +logging.level.org.springframework.boot.autoconfigure.security=DEBUG + # =============================== # SWAGGER # =============================== +springdoc.api-docs.enabled=true +springdoc.swagger-ui.enabled=true springdoc.api-docs.path=/api-docs -springdoc.swagger-ui.path=/swagger-ui/index.html +springdoc.swagger-ui.path=/swagger-ui springdoc.default-consumes-media-type=application/json springdoc.default-produces-media-type=application/json springdoc.swagger-ui.disable-swagger-default-url=true @@ -52,7 +91,7 @@ jwt.refresh-token-expiration=1209600000 # =============================== # OAUTH2 CONFIG - GOOGLE # =============================== -spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.google.redirect-uri=https://wudc.link/oauth2/callback/{registrationId} spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.google.scope=profile,email spring.security.oauth2.client.registration.google.client-name=Google @@ -65,7 +104,7 @@ spring.security.oauth2.client.provider.google.user-name-attribute=sub # =============================== # OAUTH2 CONFIG - NAVER # =============================== -spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.naver.redirect-uri=https://wudc.link/oauth2/callback/{registrationId} spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.naver.scope=name,email spring.security.oauth2.client.registration.naver.client-name=Naver @@ -78,7 +117,7 @@ spring.security.oauth2.client.provider.naver.user-name-attribute=response # =============================== # OAUTH2 CONFIG - KAKAO # =============================== -spring.security.oauth2.client.registration.kakao.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.kakao.redirect-uri=https://wudc.link/oauth2/callback/{registrationId} spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email spring.security.oauth2.client.registration.kakao.client-name=Kakao diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index fa0bd06..d4ddeba 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -38,7 +38,7 @@ logging.level.com.apptive.devlog=DEBUG # SWAGGER # =============================== springdoc.api-docs.path=/api-docs -springdoc.swagger-ui.path=/swagger-ui/index.html +springdoc.swagger-ui.path=/swagger-ui springdoc.default-consumes-media-type=application/json springdoc.default-produces-media-type=application/json springdoc.swagger-ui.disable-swagger-default-url=true diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 0000000..bc3dbf9 --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -0,0 +1,128 @@ +# =============================== +# DB CONFIG +# =============================== +spring.datasource.url=jdbc:mysql://devlog-mysql:3306/devlog?allowPublicKeyRetrieval=true&useSSL=true&verifyServerCertificate=true&requireSSL=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&sslMode=VERIFY_CA&sslCa=/certs/ca.pem&sslCert=/certs/server-cert.pem&sslKey=/certs/server-key.pem +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.username=root + +spring.datasource.hikari.data-source-properties.useSSL=true +spring.datasource.hikari.data-source-properties.requireSSL=true +spring.datasource.hikari.data-source-properties.trustCertificateKeyStoreUrl=file:/certs/truststore.p12 +spring.datasource.hikari.data-source-properties.trustCertificateKeyStoreType=PKCS12 +spring.datasource.hikari.data-source-properties.keyStore=file:/certs/keystore.p12 +spring.datasource.hikari.data-source-properties.keyStoreType=PKCS12 +spring.datasource.hikari.data-source-properties.keyAlias=devlog-app +spring.datasource.hikari.data-source-properties.clientCertificateKeyStoreUrl=file:/certs/keystore.p12 +spring.datasource.hikari.data-source-properties.clientCertificateKeyStoreType=PKCS12 + +spring.datasource.hikari.connection-timeout=30000 +spring.datasource.hikari.maximum-pool-size=10 + +# =============================== +# REDIS CONFIG +# =============================== +spring.data.redis.host=devlog-redis +spring.data.redis.port=6380 +spring.data.redis.timeout=5s +spring.data.redis.ssl.enabled=true +spring.data.redis.url=rediss://devlog-redis:6380 +spring.data.redis.client-type=jedis +spring.redis.ssl.keystore-path=/certs/keystore.p12 +spring.redis.ssl.truststore-path=/certs/truststore.p12 +spring.redis.ssl.keystore-type=PKCS12 +spring.redis.ssl.truststore-type=PKCS12s + +# =============================== +# SERVER CONFIG +# =============================== +server.address=0.0.0.0 +server.port=443 +server.ssl.enabled=true +server.ssl.key-alias=devlog-app +server.ssl.key-store=/certs/keystore.p12 +server.ssl.key-store-type=PKCS12 +server.ssl.client-auth=need +server.ssl.trust-store=/certs/truststore.p12 +server.ssl.trust-store-type=PKCS12 + +# =============================== +# JPA CONFIG +# =============================== +logging.level.org.springframework.boot.web.embedded.tomcat.SslConnectorCustomizer=DEBUG +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.show_sql=false +spring.jpa.open-in-view=false +spring.jpa.show-sql=false + +# =============================== +# LOGGING +# =============================== +logging.level.org.springframework.boot.context.config=DEBUG +logging.level.org.springframework.security=DEBUG +logging.level.com.apptive.devlog=DEBUG +logging.level.io.netty=DEBUG +logging.level.root=DEBUG +logging.level.org.springframework.web=DEBUG +logging.level.org.springframework.web.client.RestTemplate=DEBUG +logging.level.org.springframework.http=DEBUG +logging.level.org.springframework.boot.autoconfigure.security=DEBUG + + +# =============================== +# SWAGGER +# =============================== +springdoc.api-docs.enabled=true +springdoc.swagger-ui.enabled=true +springdoc.api-docs.path=/api-docs +springdoc.swagger-ui.path=/swagger-ui +springdoc.default-consumes-media-type=application/json +springdoc.default-produces-media-type=application/json +springdoc.swagger-ui.disable-swagger-default-url=true + +# =============================== +# JWT CONFIG +# =============================== +jwt.access-token-expiration=3600000 +jwt.refresh-token-expiration=1209600000 + +# =============================== +# OAUTH2 CONFIG - GOOGLE +# =============================== +spring.security.oauth2.client.registration.google.redirect-uri=https://wudc.link/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.google.scope=profile,email +spring.security.oauth2.client.registration.google.client-name=Google +spring.security.oauth2.client.registration.google.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth +spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token +spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo +spring.security.oauth2.client.provider.google.user-name-attribute=sub + +# =============================== +# OAUTH2 CONFIG - NAVER +# =============================== +spring.security.oauth2.client.registration.naver.redirect-uri=https://wudc.link/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.naver.scope=name,email +spring.security.oauth2.client.registration.naver.client-name=Naver +spring.security.oauth2.client.registration.naver.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize +spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token +spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me +spring.security.oauth2.client.provider.naver.user-name-attribute=response + +# =============================== +# OAUTH2 CONFIG - KAKAO +# =============================== +spring.security.oauth2.client.registration.kakao.redirect-uri=https://wudc.link/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email +spring.security.oauth2.client.registration.kakao.client-name=Kakao +spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post +spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize +spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token +spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me +spring.security.oauth2.client.provider.kakao.user-name-attribute=id diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 10dd667..0a8cda1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,5 +6,12 @@ spring.application.name=devlog # =============================== # PROFILE CONFIG # =============================== -spring.profiles.active=release +spring.profiles.active=local spring.profiles.include=secret + +spring.mail.host=smtp.gmail.com +spring.mail.port=587 +spring.mail.username=qqwwee771441@gmail.com +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true +spring.mail.default-encoding=UTF-8 diff --git a/start.sh b/start.sh deleted file mode 100644 index ab68b32..0000000 --- a/start.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -echo "🔄 Cleaning and building project..." -./gradlew clean build -x test - -echo "🐳 Stopping and removing existing containers..." -docker-compose down - -echo "🐳 Building Docker images..." -docker-compose build --no-cache - -echo "🚀 Starting containers..." -docker-compose up diff --git a/stop.sh b/stop.sh new file mode 100644 index 0000000..61cd526 --- /dev/null +++ b/stop.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Stop containers +docker stop devlog-mysql devlog-app devlog-redis devlog-nginx + +# Remove containers +docker rm devlog-mysql devlog-app devlog-redis devlog-nginx