diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index e30a588..11efac7 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -12,15 +12,15 @@ env: jobs: build-with-gradle: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: JDK 17 설정 - uses: actions/setup-java@v4 + - name: JDK 21 설정 + uses: actions/setup-java@v5 with: - java-version: '17' + java-version: '21' distribution: 'corretto' - name: applications.yml 환경변수 설정 @@ -42,8 +42,8 @@ jobs: - name: 도커 이미지 빌드 및 푸시 run: | - docker build -t ayeonii/leets-be:latest . - docker push ayeonii/leets-be:latest + docker build -t leetsland/leets-be:latest . + docker push leetsland/leets-be:latest deploy-dev: needs: build-with-gradle diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 5a4a480..7fa3bfb 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -2,7 +2,7 @@ name: PR Test on: pull_request: - branches: [ "main", "develop", "master" ] + branches: [ "main", "develop" ] permissions: contents: read @@ -12,12 +12,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v5 with: - java-version: '17' + java-version: '21' distribution: 'corretto' - name: Setup Gradle diff --git a/Dockerfile b/Dockerfile index f5edc52..8c929e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:17-jre +FROM amazoncorretto:21 ARG JAR_FILE=./build/libs/leets-0.0.1-SNAPSHOT.jar diff --git a/README.md b/README.md index 28a4f7f..b00e33d 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,23 @@ -# [Leets](https://leets.land) +# [Leets](https://leets.land) : 함께 도전하며 우리의 가치를 증명하는 곳 -- 가천대학교 IT 학술 동아리 `Leets`의 활동과 모집을 위한 홈페이지 레포지토리입니다. -- Elite에서 파생된 단어 Leet은 엘리트의 의미를 담고 있습니다. -- `Leets`는 여러 엘리트가 모인 공동체입니다. +
+Frame 19 -
+가천대학교 IT 창업 동아리 **Leets**의 활동과 모집을 위한 랜딩 레포지토리입니다.
-# 기술스택 -+ 프레임워크 : SpringBoot 3.0.9 -+ 언어 : Java 17 -+ 데이터베이스 : MySQL -+ 인프라 : AWS EC2 -+ CI/CD: GitHub Actions +Leets는 가천대학교 내에서 IT 서비스에 관심을 가진 이들의 첫 도전이 되고, 세상을 넓게 바라볼 수 있는 시야를 만들어주는 동아리가 되고자 합니다.
-# Environment -``` -# Cors 관련 환경변수 -CORS_ORIGIN_DEVELOPMENT= -CORS_ORIGIN_PRODUCTION= - -# Database 관련 환경변수 -DATABASE_PASSWORD= -DATABASE_URL= -DATABASE_USERNAME= - -# Oauth2 관련 환경변수 -GOOGLE_AUTH_URL= -GOOGLE_ID= -GOOGLE_LOGIN_URL= -GOOGLE_PASSWORD= -GOOGLE_REDIRECT_URL= - -# Jwt 관련 환경변수 -JWT_ACCESS_SECRET= -JWT_REFRESH_SECRET= - -# 메일 관련 환경변수 -MAIL_HOST= -MAIL_USERNAME= - -# Url 관련 환경변수 -TARGET_URL_DEV= -TARGET_URL_PROD= -``` - + + + + + +

-# 관련 Repository -[Leets 공식 홈페이지 프론트엔드](https://github.com/Leets-Official/Leets-FE) +## 🔗 관련 Repository + +- [Leets 공식 홈페이지 프론트엔드](https://github.com/Leets-Official/Leets-FE) diff --git a/build.gradle.kts b/build.gradle.kts index 39ee1de..adf18a2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,4 @@ plugins { - id("java") id("org.springframework.boot") version "4.0.0" id("io.spring.dependency-management") version "1.1.6" id("org.jetbrains.kotlin.jvm") version "2.2.0" @@ -10,17 +9,6 @@ plugins { group = "land" version = "0.0.1-SNAPSHOT" -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -configurations { - compileOnly { - extendsFrom(configurations.annotationProcessor.get()) - } -} - repositories { mavenCentral() } @@ -35,9 +23,6 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - compileOnly("org.projectlombok:lombok") - annotationProcessor("org.projectlombok:lombok") - runtimeOnly("com.mysql:mysql-connector-j") testImplementation("org.springframework.boot:spring-boot-starter-test") @@ -52,6 +37,8 @@ dependencies { implementation("com.google.api-client:google-api-client:2.8.1") + implementation("io.github.oshai:kotlin-logging-jvm:7.0.3") + testImplementation("com.squareup.okhttp3:mockwebserver:5.3.2") testImplementation("com.h2database:h2") @@ -65,20 +52,16 @@ tasks.named("test") { useJUnitPlatform() } -tasks.withType().configureEach { - options.generatedSourceOutputDirectory.set( - layout.buildDirectory.dir("generated/sources/annotationProcessor/java/main") - ) -} - tasks.withType().configureEach { failOnNoDiscoveredTests = false } kotlin { compilerOptions { - freeCompilerArgs.addAll("-Xjsr305=strict") - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) - freeCompilerArgs.set(listOf("-Xannotation-default-target=param-property")) + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) + freeCompilerArgs.addAll( + "-Xjsr305=strict", + "-Xannotation-default-target=param-property" + ) } } diff --git a/src/main/java/land/leets/domain/auth/AdminAuthDetailsService.java b/src/main/java/land/leets/domain/auth/AdminAuthDetailsService.java deleted file mode 100644 index 6fe3a47..0000000 --- a/src/main/java/land/leets/domain/auth/AdminAuthDetailsService.java +++ /dev/null @@ -1,25 +0,0 @@ -package land.leets.domain.auth; - -import land.leets.domain.admin.domain.Admin; -import land.leets.domain.admin.domain.repository.AdminRepository; -import land.leets.domain.shared.AuthRole; -import land.leets.global.error.ErrorCode; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@RequiredArgsConstructor -@Service -public class AdminAuthDetailsService implements UserDetailsService { - - private final AdminRepository adminRepository; - - @Override - public AuthDetails loadUserByUsername(String sub) throws UsernameNotFoundException { - Admin admin = this.adminRepository - .findByUsername(sub) - .orElseThrow(() -> new UsernameNotFoundException(ErrorCode.ADMIN_NOT_FOUND.getMessage())); - return new AuthDetails(admin.getId(), admin.getUsername(), AuthRole.ROLE_ADMIN); - } -} diff --git a/src/main/java/land/leets/domain/auth/AuthDetails.java b/src/main/java/land/leets/domain/auth/AuthDetails.java deleted file mode 100644 index d69494f..0000000 --- a/src/main/java/land/leets/domain/auth/AuthDetails.java +++ /dev/null @@ -1,70 +0,0 @@ -package land.leets.domain.auth; - -import land.leets.domain.shared.AuthRole; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.Collections; -import java.util.UUID; - -public class AuthDetails implements UserDetails { - - private final UUID uid; - private final String email; - private final AuthRole role; - - public AuthDetails(UUID uid, String email, AuthRole role) { - this.uid = uid; - this.email = email; - this.role = role; - } - - public UUID getUid() { - return uid; - } - - public String getEmail() { - return email; - } - - public AuthRole getRole() { - return role; - } - - @Override - public Collection getAuthorities() { - return Collections.singletonList(new SimpleGrantedAuthority(role.getRole())); - } - - @Override - public String getPassword() { - return null; - } - - @Override - public String getUsername() { - return email; - } - - @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/land/leets/domain/auth/AuthService.java b/src/main/java/land/leets/domain/auth/AuthService.java deleted file mode 100644 index f272646..0000000 --- a/src/main/java/land/leets/domain/auth/AuthService.java +++ /dev/null @@ -1,101 +0,0 @@ -package land.leets.domain.auth; - -import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; -import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.gson.GsonFactory; -import land.leets.domain.auth.exception.PermissionDeniedException; -import land.leets.domain.auth.presentation.dto.OAuthTokenDto; -import land.leets.domain.user.domain.User; -import land.leets.domain.user.domain.repository.UserRepository; -import land.leets.global.jwt.exception.InvalidTokenException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -@Service -public class AuthService { - - private final String googleAuthUrl; - private final String googleRedirectUrl; - private final String googleClientId; - private final String googleClientPassword; - private final UserRepository userRepository; - - @Autowired - public AuthService(@Value("${google.auth.url}") String googleAuthUrl, - @Value("${google.redirect.url}") String googleRedirectUrl, - @Value("${spring.security.oauth2.client.registration.google.client-id}") String googleClientId, - @Value("${spring.security.oauth2.client.registration.google.client-secret}") String googleClientPassword, - UserRepository userRepository) { - this.googleAuthUrl = googleAuthUrl; - this.googleRedirectUrl = googleRedirectUrl; - this.googleClientId = googleClientId; - this.googleClientPassword = googleClientPassword; - this.userRepository = userRepository; - } - - public User getGoogleToken(String code) throws GeneralSecurityException, IOException { - - RestTemplate restTemplate = new RestTemplate(); - Map params = new HashMap<>(); - - params.put("code", code); - params.put("client_id", googleClientId); - params.put("client_secret", googleClientPassword); - params.put("redirect_uri", googleRedirectUrl); - params.put("grant_type", "authorization_code"); - - ResponseEntity responseEntity = restTemplate.postForEntity(googleAuthUrl, params, OAuthTokenDto.class); - if (responseEntity.getStatusCode() != HttpStatus.OK || responseEntity.getBody() == null) { - throw new PermissionDeniedException(); - } - - String idToken = responseEntity.getBody().getId_token(); - return getUser(idToken); - } - - - public User getUser(String idToken) throws GeneralSecurityException, IOException { - - final GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), GsonFactory.getDefaultInstance()) - .setAudience(Collections.singletonList(googleClientId)) - .build(); - - GoogleIdToken googleIdToken = verifier.verify(idToken); - if (idToken == null) { - throw new InvalidTokenException(); - } - - Payload payload = googleIdToken.getPayload(); - - String userId = payload.getSubject(); - Optional bySub = userRepository.findBySub(userId); - - if (bySub.isPresent()) { - return bySub.get(); - } - - User user = new User( - null, - (String) payload.get("name"), - null, - payload.getEmail(), - userId, - null - ); - - return userRepository.save(user); - } -} diff --git a/src/main/java/land/leets/domain/auth/UserAuthDetailsService.java b/src/main/java/land/leets/domain/auth/UserAuthDetailsService.java deleted file mode 100644 index e2e27bf..0000000 --- a/src/main/java/land/leets/domain/auth/UserAuthDetailsService.java +++ /dev/null @@ -1,24 +0,0 @@ -package land.leets.domain.auth; - -import land.leets.domain.shared.AuthRole; -import land.leets.domain.user.domain.User; -import land.leets.domain.user.domain.repository.UserRepository; -import land.leets.global.error.ErrorCode; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class UserAuthDetailsService implements UserDetailsService { - private final UserRepository userRepository; - - @Override - public AuthDetails loadUserByUsername(String email) throws UsernameNotFoundException { - User user = this.userRepository - .findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException(ErrorCode.USER_NOT_FOUND.getMessage())); - return new AuthDetails(user.getId(), user.getEmail(), AuthRole.ROLE_USER); - } -} diff --git a/src/main/java/land/leets/domain/auth/exception/PermissionDeniedException.java b/src/main/java/land/leets/domain/auth/exception/PermissionDeniedException.java deleted file mode 100644 index 38bb7b5..0000000 --- a/src/main/java/land/leets/domain/auth/exception/PermissionDeniedException.java +++ /dev/null @@ -1,11 +0,0 @@ -package land.leets.domain.auth.exception; - - -import land.leets.global.error.ErrorCode; -import land.leets.global.error.exception.ServiceException; - -public class PermissionDeniedException extends ServiceException { - public PermissionDeniedException() { - super(ErrorCode.PERMISSION_DENIED); - } -} diff --git a/src/main/java/land/leets/domain/auth/presentation/AuthController.java b/src/main/java/land/leets/domain/auth/presentation/AuthController.java deleted file mode 100644 index 923dd5c..0000000 --- a/src/main/java/land/leets/domain/auth/presentation/AuthController.java +++ /dev/null @@ -1,51 +0,0 @@ -package land.leets.domain.auth.presentation; - -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 land.leets.domain.auth.AuthService; -import land.leets.domain.shared.AuthRole; -import land.leets.domain.user.domain.User; -import land.leets.global.error.ErrorResponse; -import land.leets.global.jwt.JwtProvider; -import land.leets.global.jwt.dto.JwtResponse; -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.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.io.IOException; -import java.security.GeneralSecurityException; - -@RestController -@RequiredArgsConstructor -public class AuthController { - - private final AuthService authService; - private final JwtProvider jwtProvider; - - @Operation(summary = "(로그인) 유저 로그인", description = "구글 토큰으로 로그인합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse(responseCode = "500", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) - }) - @GetMapping("/login/oauth2/callback/google") - public JwtResponse get(@RequestParam("code") String code) throws GeneralSecurityException, IOException { - User user = authService.getGoogleToken(code); - String accessToken = this.jwtProvider.generateToken(user.getId(), user.getEmail(), AuthRole.ROLE_USER, false); - String refreshToken = this.jwtProvider.generateToken(user.getId(), user.getEmail(), AuthRole.ROLE_USER, true); - - return new JwtResponse(accessToken, refreshToken); - } - - @GetMapping("/health-check") - public ResponseEntity checkHealthStatus() { - return new ResponseEntity<>(HttpStatus.OK); - } -} diff --git a/src/main/java/land/leets/domain/auth/presentation/dto/OAuthTokenDto.java b/src/main/java/land/leets/domain/auth/presentation/dto/OAuthTokenDto.java deleted file mode 100644 index 39bfc4b..0000000 --- a/src/main/java/land/leets/domain/auth/presentation/dto/OAuthTokenDto.java +++ /dev/null @@ -1,14 +0,0 @@ -package land.leets.domain.auth.presentation.dto; - -import lombok.*; - -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Getter -public class OAuthTokenDto { - private String access_token; - private String expires_in; - private String scope; - private String token_type; - private String id_token; -} diff --git a/src/main/java/land/leets/domain/image/presentation/ImageController.java b/src/main/java/land/leets/domain/image/presentation/ImageController.java deleted file mode 100644 index 9e6fc80..0000000 --- a/src/main/java/land/leets/domain/image/presentation/ImageController.java +++ /dev/null @@ -1,27 +0,0 @@ -package land.leets.domain.image.presentation; - -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/images") -public class ImageController { - - @Value("${image.path}") - private String imageStoragePath; - - @GetMapping("/{imageName}") - public ResponseEntity getImage(@PathVariable String imageName) { - Resource resource = new ClassPathResource(imageStoragePath + imageName); - return new ResponseEntity<>(resource, HttpStatus.OK); - } -} diff --git a/src/main/java/land/leets/domain/shared/BaseTimeEntity.java b/src/main/java/land/leets/domain/shared/BaseTimeEntity.java deleted file mode 100644 index 3593d89..0000000 --- a/src/main/java/land/leets/domain/shared/BaseTimeEntity.java +++ /dev/null @@ -1,33 +0,0 @@ -package land.leets.domain.shared; - -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; - -@Getter -@EntityListeners(AuditingEntityListener.class) -@MappedSuperclass -public class BaseTimeEntity { - - @CreatedDate - @Column(updatable = false) - private LocalDateTime createdAt; - - @LastModifiedDate - @Column(updatable = false) - private LocalDateTime updatedAt; - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } -} diff --git a/src/main/java/land/leets/domain/shared/exception/PasswordNotMatchException.java b/src/main/java/land/leets/domain/shared/exception/PasswordNotMatchException.java deleted file mode 100644 index d9e0e97..0000000 --- a/src/main/java/land/leets/domain/shared/exception/PasswordNotMatchException.java +++ /dev/null @@ -1,10 +0,0 @@ -package land.leets.domain.shared.exception; - -import land.leets.global.error.ErrorCode; -import land.leets.global.error.exception.ServiceException; - -public class PasswordNotMatchException extends ServiceException { - public PasswordNotMatchException() { - super(ErrorCode.PASSWORD_NOT_MATCH); - } -} diff --git a/src/main/java/land/leets/global/advice/ResponseAdvice.java b/src/main/java/land/leets/global/advice/ResponseAdvice.java deleted file mode 100644 index 40671f9..0000000 --- a/src/main/java/land/leets/global/advice/ResponseAdvice.java +++ /dev/null @@ -1,36 +0,0 @@ -package land.leets.global.advice; - -import org.springframework.core.MethodParameter; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; - -import java.util.HashMap; -import java.util.Map; - -@RestControllerAdvice -public class ResponseAdvice implements ResponseBodyAdvice { - - private static final String API_DOCS_PATH = "/v3/api-docs"; - private static final String IMAGE_PATH = "/images"; - - @Override - public boolean supports(MethodParameter returnType, Class> converterType) { - return true; - } - - @Override - public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, - ServerHttpRequest req, ServerHttpResponse res) { - if (req.getURI().getPath().contains(API_DOCS_PATH)) return body; - if (req.getURI().getPath().contains(IMAGE_PATH)) return body; - - Map updatedResponse = new HashMap<>(); - - updatedResponse.put("result", body); - return updatedResponse; - } -} diff --git a/src/main/java/land/leets/global/config/AsyncConfig.java b/src/main/java/land/leets/global/config/AsyncConfig.java deleted file mode 100644 index 220fe2d..0000000 --- a/src/main/java/land/leets/global/config/AsyncConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package land.leets.global.config; - -import java.util.concurrent.Executor; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@EnableAsync -@Configuration -public class AsyncConfig { - - @Bean - public Executor taskExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(2); - executor.setMaxPoolSize(2); - executor.setQueueCapacity(200); - executor.setThreadNamePrefix("Mailing-"); - executor.setKeepAliveSeconds(60); - executor.setRejectedExecutionHandler(((r, asyncExecutor) -> log.warn("더 이상 요청을 처리할 수 없습니다."))); - executor.initialize(); - return executor; - } -} diff --git a/src/main/java/land/leets/global/config/DatabaseSetup.java b/src/main/java/land/leets/global/config/DatabaseSetup.java deleted file mode 100644 index cdd1216..0000000 --- a/src/main/java/land/leets/global/config/DatabaseSetup.java +++ /dev/null @@ -1,20 +0,0 @@ -package land.leets.global.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class DatabaseSetup implements ApplicationRunner { - - private final JdbcTemplate jdbcTemplate; - - @Override - public void run(ApplicationArguments args) { - jdbcTemplate.execute("ALTER DATABASE leets CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci"); - jdbcTemplate.execute("ALTER TABLE portfolios CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); - } -} diff --git a/src/main/java/land/leets/global/config/WebClientConfig.java b/src/main/java/land/leets/global/config/WebClientConfig.java deleted file mode 100644 index de83bb8..0000000 --- a/src/main/java/land/leets/global/config/WebClientConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package land.leets.global.config; - -import io.netty.channel.ChannelOption; -import io.netty.handler.timeout.ReadTimeoutHandler; -import io.netty.handler.timeout.WriteTimeoutHandler; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.reactive.ClientHttpConnector; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.netty.http.client.HttpClient; - -import java.time.Duration; - -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient() { - HttpClient httpClient = HttpClient.create() - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000) - .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(10)) - .addHandlerLast(new WriteTimeoutHandler(10))) - .responseTimeout(Duration.ofSeconds(1)); - - ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient); - return WebClient.builder().clientConnector(connector).build(); - } -} diff --git a/src/main/java/land/leets/global/swagger/SwaggerConfig.java b/src/main/java/land/leets/global/swagger/SwaggerConfig.java deleted file mode 100644 index 0146e0a..0000000 --- a/src/main/java/land/leets/global/swagger/SwaggerConfig.java +++ /dev/null @@ -1,35 +0,0 @@ -package land.leets.global.swagger; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.security.SecurityRequirement; -import io.swagger.v3.oas.models.security.SecurityScheme; -import io.swagger.v3.oas.models.servers.Server; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@OpenAPIDefinition(info = @Info( - title = "Leets API", - description = "Leets API 문서", - version = "v1.0.0")) -@Configuration -public class SwaggerConfig { - @Bean - public OpenAPI openApi() { - String jwt = "JWT"; - SecurityRequirement securityRequirement = new SecurityRequirement().addList("JWT"); - Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme() - .name(jwt) - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") - ); - - return new OpenAPI() - .addServersItem(new Server().url("/")) - .addSecurityItem(securityRequirement) - .components(components); - } -} diff --git a/src/main/java/land/leets/LeetsApplication.java b/src/main/kotlin/land/leets/LeetsApplication.kt similarity index 53% rename from src/main/java/land/leets/LeetsApplication.java rename to src/main/kotlin/land/leets/LeetsApplication.kt index 818598e..577931e 100644 --- a/src/main/java/land/leets/LeetsApplication.java +++ b/src/main/kotlin/land/leets/LeetsApplication.kt @@ -1,17 +1,15 @@ -package land.leets; +package land.leets -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.data.jpa.repository.config.EnableJpaAuditing +import org.springframework.scheduling.annotation.EnableScheduling @SpringBootApplication @EnableJpaAuditing @EnableScheduling -public class LeetsApplication { - - public static void main(String[] args) { - SpringApplication.run(LeetsApplication.class, args); - } +class LeetsApplication +fun main(args: Array) { + runApplication(*args) } diff --git a/src/main/kotlin/land/leets/domain/admin/domain/repository/AdminRepository.kt b/src/main/kotlin/land/leets/domain/admin/domain/repository/AdminRepository.kt index 2553ee9..8016f25 100644 --- a/src/main/kotlin/land/leets/domain/admin/domain/repository/AdminRepository.kt +++ b/src/main/kotlin/land/leets/domain/admin/domain/repository/AdminRepository.kt @@ -2,9 +2,8 @@ package land.leets.domain.admin.domain.repository import land.leets.domain.admin.domain.Admin import org.springframework.data.jpa.repository.JpaRepository -import java.util.Optional import java.util.UUID interface AdminRepository : JpaRepository { - fun findByUsername(username: String): Optional -} \ No newline at end of file + fun findByUsername(username: String): Admin? +} diff --git a/src/main/kotlin/land/leets/domain/admin/usecase/AdminLoginImpl.kt b/src/main/kotlin/land/leets/domain/admin/usecase/AdminLoginImpl.kt index ad6adbf..bf83e25 100644 --- a/src/main/kotlin/land/leets/domain/admin/usecase/AdminLoginImpl.kt +++ b/src/main/kotlin/land/leets/domain/admin/usecase/AdminLoginImpl.kt @@ -17,7 +17,7 @@ class AdminLoginImpl( ) : AdminLogin { override fun execute(username: String, password: String): JwtResponse { - val admin = adminRepository.findByUsername(username).orElseThrow { AdminNotFoundException() } + val admin = adminRepository.findByUsername(username) ?: throw AdminNotFoundException() if (!passwordEncoder.matches(password, admin.password)) { throw PasswordNotMatchException() diff --git a/src/main/kotlin/land/leets/domain/auth/AdminAuthDetailsService.kt b/src/main/kotlin/land/leets/domain/auth/AdminAuthDetailsService.kt new file mode 100644 index 0000000..c8b6dda --- /dev/null +++ b/src/main/kotlin/land/leets/domain/auth/AdminAuthDetailsService.kt @@ -0,0 +1,21 @@ +package land.leets.domain.auth + +import land.leets.domain.admin.domain.repository.AdminRepository +import land.leets.domain.shared.AuthRole +import land.leets.global.error.ErrorCode +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.stereotype.Service + +@Service +class AdminAuthDetailsService( + private val adminRepository: AdminRepository +) : UserDetailsService { + + override fun loadUserByUsername(sub: String): AuthDetails { + val admin = adminRepository.findByUsername(sub) + ?: throw UsernameNotFoundException(ErrorCode.ADMIN_NOT_FOUND.message) + + return AuthDetails(admin.id!!, admin.username, AuthRole.ROLE_ADMIN) + } +} diff --git a/src/main/kotlin/land/leets/domain/auth/AuthDetails.kt b/src/main/kotlin/land/leets/domain/auth/AuthDetails.kt new file mode 100644 index 0000000..9f5afd5 --- /dev/null +++ b/src/main/kotlin/land/leets/domain/auth/AuthDetails.kt @@ -0,0 +1,30 @@ +package land.leets.domain.auth + +import land.leets.domain.shared.AuthRole +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import java.util.UUID + +class AuthDetails( + val uid: UUID, + val email: String, + val role: AuthRole +) : UserDetails { + + override fun getAuthorities(): Collection { + return listOf(SimpleGrantedAuthority(role.role)) + } + + override fun getPassword(): String? = null + + override fun getUsername(): String = email + + override fun isAccountNonExpired(): Boolean = true + + override fun isAccountNonLocked(): Boolean = true + + override fun isCredentialsNonExpired(): Boolean = true + + override fun isEnabled(): Boolean = true +} diff --git a/src/main/kotlin/land/leets/domain/auth/AuthService.kt b/src/main/kotlin/land/leets/domain/auth/AuthService.kt new file mode 100644 index 0000000..5f3319a --- /dev/null +++ b/src/main/kotlin/land/leets/domain/auth/AuthService.kt @@ -0,0 +1,77 @@ +package land.leets.domain.auth + +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier +import com.google.api.client.http.javanet.NetHttpTransport +import com.google.api.client.json.gson.GsonFactory +import io.github.oshai.kotlinlogging.KotlinLogging +import land.leets.domain.auth.exception.PermissionDeniedException +import land.leets.domain.auth.presentation.dto.OAuthTokenDto +import land.leets.domain.user.domain.User +import land.leets.domain.user.domain.repository.UserRepository +import land.leets.global.jwt.exception.InvalidTokenException +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.web.client.RestTemplate + +private val log = KotlinLogging.logger {} + +@Service +class AuthService( + @Value("\${google.auth.url}") private val googleAuthUrl: String, + @Value("\${google.redirect.url}") private val googleRedirectUrl: String, + @Value("\${spring.security.oauth2.client.registration.google.client-id}") private val googleClientId: String, + @Value("\${spring.security.oauth2.client.registration.google.client-secret}") private val googleClientPassword: String, + private val userRepository: UserRepository, +) { + + fun getGoogleToken(code: String): User { + val restTemplate = RestTemplate() + val params = mapOf( + "code" to code, + "client_id" to googleClientId, + "client_secret" to googleClientPassword, + "redirect_uri" to googleRedirectUrl, + "grant_type" to "authorization_code" + ) + + val responseEntity = restTemplate.postForEntity(googleAuthUrl, params, OAuthTokenDto::class.java) + + if (responseEntity.statusCode != HttpStatus.OK || responseEntity.body == null) { + throw PermissionDeniedException() + } + + val idToken = responseEntity.body!!.idToken + return getUser(idToken) + } + + fun getUser(idToken: String): User { + val verifier = GoogleIdTokenVerifier.Builder(NetHttpTransport(), GsonFactory.getDefaultInstance()) + .setAudience(listOf(googleClientId)) + .build() + + val googleIdToken = runCatching { + verifier.verify(idToken) + }.onFailure { e -> + log.debug { + "${"Google ID 토큰 인증 실패: {}"} ${ + e::class.simpleName + }" + } + }.getOrNull() ?: throw InvalidTokenException() + + + val payload = googleIdToken.payload + val userId = payload.subject + + return userRepository.findBySub(userId) ?: userRepository.save( + User( + sid = null, + name = payload["name"].toString(), + phone = null, + email = payload.email, + sub = userId + ) + ) + } +} diff --git a/src/main/kotlin/land/leets/domain/auth/UserAuthDetailsService.kt b/src/main/kotlin/land/leets/domain/auth/UserAuthDetailsService.kt new file mode 100644 index 0000000..bfef56f --- /dev/null +++ b/src/main/kotlin/land/leets/domain/auth/UserAuthDetailsService.kt @@ -0,0 +1,21 @@ +package land.leets.domain.auth + +import land.leets.domain.shared.AuthRole +import land.leets.domain.user.domain.repository.UserRepository +import land.leets.global.error.ErrorCode +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.stereotype.Service + +@Service +class UserAuthDetailsService( + private val userRepository: UserRepository +) : UserDetailsService { + + override fun loadUserByUsername(email: String): AuthDetails { + val user = userRepository.findByEmail(email) + ?: throw UsernameNotFoundException(ErrorCode.USER_NOT_FOUND.message) + + return AuthDetails(user.id!!, user.email, AuthRole.ROLE_USER) + } +} diff --git a/src/main/kotlin/land/leets/domain/auth/exception/PermissionDeniedException.kt b/src/main/kotlin/land/leets/domain/auth/exception/PermissionDeniedException.kt new file mode 100644 index 0000000..9cf1433 --- /dev/null +++ b/src/main/kotlin/land/leets/domain/auth/exception/PermissionDeniedException.kt @@ -0,0 +1,6 @@ +package land.leets.domain.auth.exception + +import land.leets.global.error.ErrorCode +import land.leets.global.error.exception.ServiceException + +class PermissionDeniedException : ServiceException(ErrorCode.PERMISSION_DENIED) diff --git a/src/main/kotlin/land/leets/domain/auth/presentation/AuthController.kt b/src/main/kotlin/land/leets/domain/auth/presentation/AuthController.kt new file mode 100644 index 0000000..db59255 --- /dev/null +++ b/src/main/kotlin/land/leets/domain/auth/presentation/AuthController.kt @@ -0,0 +1,56 @@ +package land.leets.domain.auth.presentation + +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 land.leets.domain.auth.AuthService +import land.leets.domain.shared.AuthRole +import land.leets.global.error.ErrorResponse +import land.leets.global.jwt.JwtProvider +import land.leets.global.jwt.dto.JwtResponse +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +class AuthController( + private val authService: AuthService, + private val jwtProvider: JwtProvider +) { + + @Operation(summary = "(로그인) 유저 로그인", description = "구글 토큰으로 로그인합니다.") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200"), + ApiResponse( + responseCode = "400", + content = [Content(schema = Schema(implementation = ErrorResponse::class))] + ), + ApiResponse( + responseCode = "404", + content = [Content(schema = Schema(implementation = ErrorResponse::class))] + ), + ApiResponse( + responseCode = "500", + content = [Content(schema = Schema(implementation = ErrorResponse::class))] + ) + ] + ) + @GetMapping("/login/oauth2/callback/google") + fun get(@RequestParam("code") code: String): JwtResponse { + val user = authService.getGoogleToken(code) + val accessToken = jwtProvider.generateToken(user.id!!, user.email, AuthRole.ROLE_USER, false) + val refreshToken = jwtProvider.generateToken(user.id, user.email, AuthRole.ROLE_USER, true) + + return JwtResponse(accessToken, refreshToken) + } + + @GetMapping("/health-check") + fun checkHealthStatus(): ResponseEntity { + return ResponseEntity(HttpStatus.OK) + } +} diff --git a/src/main/kotlin/land/leets/domain/auth/presentation/dto/OAuthTokenDto.kt b/src/main/kotlin/land/leets/domain/auth/presentation/dto/OAuthTokenDto.kt new file mode 100644 index 0000000..a289c74 --- /dev/null +++ b/src/main/kotlin/land/leets/domain/auth/presentation/dto/OAuthTokenDto.kt @@ -0,0 +1,11 @@ +package land.leets.domain.auth.presentation.dto + +import com.fasterxml.jackson.annotation.JsonProperty + +data class OAuthTokenDto( + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("expires_in") val expiresIn: String, + val scope: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("id_token") val idToken: String +) diff --git a/src/main/kotlin/land/leets/domain/comment/presentation/dto/CommentResponse.kt b/src/main/kotlin/land/leets/domain/comment/presentation/dto/CommentResponse.kt index 72607e2..f4ed25e 100644 --- a/src/main/kotlin/land/leets/domain/comment/presentation/dto/CommentResponse.kt +++ b/src/main/kotlin/land/leets/domain/comment/presentation/dto/CommentResponse.kt @@ -7,9 +7,9 @@ import java.time.LocalDateTime data class CommentResponse( val content: String, - val createdAt: LocalDateTime, + val createdAt: LocalDateTime?, - val updatedAt: LocalDateTime, + val updatedAt: LocalDateTime?, val admin: AdminDetailsResponse ) { diff --git a/src/main/kotlin/land/leets/domain/image/presentation/ImageController.kt b/src/main/kotlin/land/leets/domain/image/presentation/ImageController.kt new file mode 100644 index 0000000..7f98d6d --- /dev/null +++ b/src/main/kotlin/land/leets/domain/image/presentation/ImageController.kt @@ -0,0 +1,24 @@ +package land.leets.domain.image.presentation + +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.io.ClassPathResource +import org.springframework.core.io.Resource +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/images") +class ImageController( + @Value("\${image.path}") private val imageStoragePath: String +) { + + @GetMapping("/{imageName}") + fun getImage(@PathVariable imageName: String): ResponseEntity { + val resource = ClassPathResource(imageStoragePath + imageName) + return ResponseEntity(resource, HttpStatus.OK) + } +} diff --git a/src/main/kotlin/land/leets/domain/shared/BaseTimeEntity.kt b/src/main/kotlin/land/leets/domain/shared/BaseTimeEntity.kt new file mode 100644 index 0000000..ab5ee20 --- /dev/null +++ b/src/main/kotlin/land/leets/domain/shared/BaseTimeEntity.kt @@ -0,0 +1,22 @@ +package land.leets.domain.shared + +import jakarta.persistence.Column +import jakarta.persistence.EntityListeners +import jakarta.persistence.MappedSuperclass +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) +abstract class BaseTimeEntity { + + @CreatedDate + @Column(updatable = false) + var createdAt: LocalDateTime? = null + + @LastModifiedDate + @Column + var updatedAt: LocalDateTime? = null +} diff --git a/src/main/kotlin/land/leets/domain/shared/exception/PasswordNotMatchException.kt b/src/main/kotlin/land/leets/domain/shared/exception/PasswordNotMatchException.kt new file mode 100644 index 0000000..bbe703b --- /dev/null +++ b/src/main/kotlin/land/leets/domain/shared/exception/PasswordNotMatchException.kt @@ -0,0 +1,6 @@ +package land.leets.domain.shared.exception + +import land.leets.global.error.ErrorCode +import land.leets.global.error.exception.ServiceException + +class PasswordNotMatchException : ServiceException(ErrorCode.PASSWORD_NOT_MATCH) diff --git a/src/main/kotlin/land/leets/domain/user/domain/repository/UserRepository.kt b/src/main/kotlin/land/leets/domain/user/domain/repository/UserRepository.kt index d14f84b..952dc88 100644 --- a/src/main/kotlin/land/leets/domain/user/domain/repository/UserRepository.kt +++ b/src/main/kotlin/land/leets/domain/user/domain/repository/UserRepository.kt @@ -2,10 +2,9 @@ package land.leets.domain.user.domain.repository import land.leets.domain.user.domain.User import org.springframework.data.jpa.repository.JpaRepository -import java.util.Optional import java.util.UUID interface UserRepository : JpaRepository { - fun findBySub(sub: String): Optional - fun findByEmail(email: String): Optional + fun findBySub(sub: String): User? + fun findByEmail(email: String): User? } diff --git a/src/main/kotlin/land/leets/global/advise/ExceptionHandleAdvice.kt b/src/main/kotlin/land/leets/global/advice/ExceptionHandleAdvice.kt similarity index 98% rename from src/main/kotlin/land/leets/global/advise/ExceptionHandleAdvice.kt rename to src/main/kotlin/land/leets/global/advice/ExceptionHandleAdvice.kt index 77a758a..bb69514 100644 --- a/src/main/kotlin/land/leets/global/advise/ExceptionHandleAdvice.kt +++ b/src/main/kotlin/land/leets/global/advice/ExceptionHandleAdvice.kt @@ -1,4 +1,4 @@ -package land.leets.global.advise +package land.leets.global.advice import land.leets.global.error.ErrorCode import land.leets.global.error.ErrorResponse diff --git a/src/main/kotlin/land/leets/global/advice/ResponseAdvice.kt b/src/main/kotlin/land/leets/global/advice/ResponseAdvice.kt new file mode 100644 index 0000000..147b6d4 --- /dev/null +++ b/src/main/kotlin/land/leets/global/advice/ResponseAdvice.kt @@ -0,0 +1,41 @@ +package land.leets.global.advice + +import org.springframework.core.MethodParameter +import org.springframework.http.MediaType +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.http.server.ServerHttpRequest +import org.springframework.http.server.ServerHttpResponse +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice + +@RestControllerAdvice +class ResponseAdvice : ResponseBodyAdvice { + + companion object { + private const val API_DOCS_PATH = "/v3/api-docs" + private const val IMAGE_PATH = "/images" + } + + override fun supports( + returnType: MethodParameter, + converterType: Class> + ): Boolean { + return true + } + + override fun beforeBodyWrite( + body: Any?, + returnType: MethodParameter, + selectedContentType: MediaType, + selectedConverterType: Class>, + req: ServerHttpRequest, + res: ServerHttpResponse + ): Any? { + if (req.uri.path.contains(API_DOCS_PATH)) return body + if (req.uri.path.contains(IMAGE_PATH)) return body + + val updatedResponse = mutableMapOf() + updatedResponse["result"] = body + return updatedResponse + } +} diff --git a/src/main/kotlin/land/leets/global/config/DatabaseSetup.kt b/src/main/kotlin/land/leets/global/config/DatabaseSetup.kt new file mode 100644 index 0000000..8d8ca55 --- /dev/null +++ b/src/main/kotlin/land/leets/global/config/DatabaseSetup.kt @@ -0,0 +1,17 @@ +package land.leets.global.config + +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Component + +@Component +class DatabaseSetup( + private val jdbcTemplate: JdbcTemplate +) : ApplicationRunner { + + override fun run(args: ApplicationArguments) { + jdbcTemplate.execute("ALTER DATABASE leets CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci") + jdbcTemplate.execute("ALTER TABLE portfolios CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci") + } +} diff --git a/src/main/kotlin/land/leets/global/config/WebClientConfig.kt b/src/main/kotlin/land/leets/global/config/WebClientConfig.kt new file mode 100644 index 0000000..f386b61 --- /dev/null +++ b/src/main/kotlin/land/leets/global/config/WebClientConfig.kt @@ -0,0 +1,29 @@ +package land.leets.global.config + +import io.netty.channel.ChannelOption +import io.netty.handler.timeout.ReadTimeoutHandler +import io.netty.handler.timeout.WriteTimeoutHandler +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.client.reactive.ReactorClientHttpConnector +import org.springframework.web.reactive.function.client.WebClient +import reactor.netty.http.client.HttpClient +import java.time.Duration + +@Configuration +class WebClientConfig { + + @Bean + fun webClient(): WebClient { + val httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000) + .doOnConnected { connection -> + connection.addHandlerLast(ReadTimeoutHandler(10)) + .addHandlerLast(WriteTimeoutHandler(10)) + } + .responseTimeout(Duration.ofSeconds(1)) + + val connector = ReactorClientHttpConnector(httpClient) + return WebClient.builder().clientConnector(connector).build() + } +} diff --git a/src/main/kotlin/land/leets/global/swagger/SwaggerConfig.kt b/src/main/kotlin/land/leets/global/swagger/SwaggerConfig.kt new file mode 100644 index 0000000..cfae7d8 --- /dev/null +++ b/src/main/kotlin/land/leets/global/swagger/SwaggerConfig.kt @@ -0,0 +1,41 @@ +package land.leets.global.swagger + +import io.swagger.v3.oas.annotations.OpenAPIDefinition +import io.swagger.v3.oas.annotations.info.Info +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.security.SecurityRequirement +import io.swagger.v3.oas.models.security.SecurityScheme +import io.swagger.v3.oas.models.servers.Server +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@OpenAPIDefinition( + info = Info( + title = "Leets API", + description = "leets.land API 문서", + version = "v2.0.0" + ) +) +@Configuration +class SwaggerConfig { + + @Bean + fun openApi(): OpenAPI { + val jwt = "JWT" + val securityRequirement = SecurityRequirement().addList("JWT") + val components = Components().addSecuritySchemes( + jwt, + SecurityScheme() + .name(jwt) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + ) + + return OpenAPI() + .addServersItem(Server().url("/")) + .addSecurityItem(securityRequirement) + .components(components) + } +} diff --git a/src/test/java/land/leets/LeetsApplicationTests.java b/src/test/java/land/leets/LeetsApplicationTests.java deleted file mode 100644 index 1fc9bea..0000000 --- a/src/test/java/land/leets/LeetsApplicationTests.java +++ /dev/null @@ -1,7 +0,0 @@ -package land.leets; - -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class LeetsApplicationTests { -} diff --git a/src/test/kotlin/land/leets/LeetsApplicationTests.kt b/src/test/kotlin/land/leets/LeetsApplicationTests.kt new file mode 100644 index 0000000..3ff3a85 --- /dev/null +++ b/src/test/kotlin/land/leets/LeetsApplicationTests.kt @@ -0,0 +1,7 @@ +package land.leets + +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class LeetsApplicationTests { +} diff --git a/src/test/kotlin/land/leets/domain/admin/usecase/AdminLoginImplTest.kt b/src/test/kotlin/land/leets/domain/admin/usecase/AdminLoginImplTest.kt index 973a738..f64f061 100644 --- a/src/test/kotlin/land/leets/domain/admin/usecase/AdminLoginImplTest.kt +++ b/src/test/kotlin/land/leets/domain/admin/usecase/AdminLoginImplTest.kt @@ -12,7 +12,6 @@ import land.leets.domain.shared.AuthRole import land.leets.domain.shared.exception.PasswordNotMatchException import land.leets.global.jwt.JwtProvider import org.springframework.security.crypto.password.PasswordEncoder -import java.util.Optional import java.util.UUID class AdminLoginImplTest : DescribeSpec({ @@ -31,7 +30,7 @@ class AdminLoginImplTest : DescribeSpec({ val admin = Admin(username, encodedPassword, "name", "email", id) it("로그인이 성공하면 JwtResponse를 반환한다") { - every { adminRepository.findByUsername(username) } returns Optional.of(admin) + every { adminRepository.findByUsername(username) } returns admin every { passwordEncoder.matches(password, encodedPassword) } returns true every { jwtProvider.generateToken(id, username, AuthRole.ROLE_ADMIN, false) } returns "accessToken" every { jwtProvider.generateToken(id, username, AuthRole.ROLE_ADMIN, true) } returns "refreshToken" @@ -43,7 +42,7 @@ class AdminLoginImplTest : DescribeSpec({ } it("관리자를 찾을 수 없으면 AdminNotFoundException을 던진다") { - every { adminRepository.findByUsername(username) } returns Optional.empty() + every { adminRepository.findByUsername(username) } returns null shouldThrow { adminLogin.execute(username, password) @@ -51,7 +50,7 @@ class AdminLoginImplTest : DescribeSpec({ } it("비밀번호가 일치하지 않으면 PasswordNotMatchException을 던진다") { - every { adminRepository.findByUsername(username) } returns Optional.of(admin) + every { adminRepository.findByUsername(username) } returns admin every { passwordEncoder.matches(password, encodedPassword) } returns false shouldThrow { diff --git a/src/test/kotlin/land/leets/domain/application/usecase/CreateApplicationImplTest.kt b/src/test/kotlin/land/leets/domain/application/usecase/CreateApplicationImplTest.kt index e6dc297..86665b1 100644 --- a/src/test/kotlin/land/leets/domain/application/usecase/CreateApplicationImplTest.kt +++ b/src/test/kotlin/land/leets/domain/application/usecase/CreateApplicationImplTest.kt @@ -13,6 +13,7 @@ import land.leets.domain.application.presentation.dto.ApplicationRequest import land.leets.domain.application.type.Position import land.leets.domain.application.type.SubmitStatus import land.leets.domain.auth.AuthDetails +import land.leets.domain.shared.AuthRole import land.leets.domain.user.domain.User import land.leets.domain.user.domain.repository.UserRepository import java.util.* @@ -26,9 +27,7 @@ class CreateApplicationImplTest : DescribeSpec({ describe("CreateApplicationImpl 유스케이스는") { context("지원서 생성을 요청할 때") { val uid = UUID.randomUUID() - val authDetails = mockk { - every { getUid() } returns uid - } + val authDetails = AuthDetails(uid, "test@test.com", AuthRole.ROLE_USER) val request = ApplicationRequest( name = "Test", sid = "20202020", diff --git a/src/test/kotlin/land/leets/domain/application/usecase/UpdateApplicationImplTest.kt b/src/test/kotlin/land/leets/domain/application/usecase/UpdateApplicationImplTest.kt index 3d10d31..bebb964 100644 --- a/src/test/kotlin/land/leets/domain/application/usecase/UpdateApplicationImplTest.kt +++ b/src/test/kotlin/land/leets/domain/application/usecase/UpdateApplicationImplTest.kt @@ -10,6 +10,7 @@ import land.leets.domain.application.presentation.dto.ApplicationRequest import land.leets.domain.application.type.Position import land.leets.domain.application.type.SubmitStatus import land.leets.domain.auth.AuthDetails +import land.leets.domain.shared.AuthRole import land.leets.domain.user.usecase.UpdateUser import java.util.* @@ -22,9 +23,7 @@ class UpdateApplicationImplTest : DescribeSpec({ describe("UpdateApplicationImpl 유스케이스는") { context("지원서 수정을 요청할 때") { val uid = UUID.randomUUID() - val authDetails = mockk { - every { getUid() } returns uid - } + val authDetails = AuthDetails(uid, "test@test.com", AuthRole.ROLE_USER) val request = ApplicationRequest( name = "Test Updated", sid = "20202020",