diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 00000000..779dd7ff --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,75 @@ +# github repository actions 페이지에 나타날 이름 +name: CI/CD using github actions & docker + +# event trigger +on: + push: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + CI-CD: + runs-on: ubuntu-latest + steps: + + # JDK setting + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: set environment variables + working-directory: ./ + run: | + touch src/main/resources/application-secret.properties + echo ${{ secrets.ENV }} >> src/main/resources/application-secret.properties + + # gradle caching - 빌드 시간 향상 + - name: Gradle Caching + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + # docker login + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # build gradle + - name: Build gradlew + run: | + chmod +x gradlew + ./gradlew jib \ + -Djib.from.auth.username=${{ secrets.DOCKER_USERNAME }} \ + -Djib.from.auth.password=${{ secrets.DOCKER_PASSWORD }} \ + -Dspring.profiles.active=prod + + ## deploy to production + - name: Deploy to prod + uses: appleboy/ssh-action@master + id: deploy-prod + if: contains(github.ref, 'main') + with: + host: ${{ secrets.AWS_PUBLIC_IP }} + username: ${{ secrets.AWS_USER }} + key: ${{ secrets.AWS_PRIVATE_KEY }} + port: ${{ secrets.AWS_SSH_PORT }} + script: | + sudo docker pull ${{ secrets.DOCKER_USERNAME }}/server + sudo docker stop stumeet-server + docker container prune -f + sudo docker-compose up -d --no-deps --force-recreate stumeet-server + sudo docker image prune -f diff --git a/.gitignore b/.gitignore index c2065bc2..6e680609 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +application-secret.properties \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1c7b59bc..35fe7776 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'org.springframework.boot' version '3.2.2' id 'io.spring.dependency-management' version '1.1.4' id 'org.asciidoctor.jvm.convert' version '3.3.2' + id 'com.google.cloud.tools.jib' version '3.4.0' } group = 'com.stumeet' @@ -28,23 +29,37 @@ ext { } dependencies { + // jwt 의존성 추가 + implementation 'io.jsonwebtoken:jjwt-api:0.12.5' + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' + implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' +// implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.flywaydb:flyway-core' - implementation 'org.flywaydb:flyway-mysql' - implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.flywaydb:flyway-mysql' + implementation 'org.flywaydb:flyway-core' + annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' testImplementation 'org.springframework.security:spring-security-test' + + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" // 버전 변경 + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + } dependencyManagement { @@ -62,3 +77,16 @@ tasks.named('asciidoctor') { inputs.dir snippetsDir dependsOn test } + +jib { + from { + image = "eclipse-temurin:21" + } + to { + image = "stumeet/server" + tags = [version] + } + container { + jvmFlags = ["-Xms128m", "-Xmx128m"] + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..4e64ee92 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.8' + +networks: + server-connection: + driver: bridge + +services: + nginx: + container_name: nginx + image: nginx + restart: always + ports: + - '80:80' + - '443:443' + networks: + - server-connection + environment: + - TZ=Asia/Seoul + depends_on: + - stumeet-server + + stumeet-server: + container_name: stumeet-server + image: stumeet/server + restart: always + expose: + - '8080' + networks: + - server-connection \ No newline at end of file diff --git a/src/main/java/com/stumeet/server/ServerApplication.java b/src/main/java/com/stumeet/server/ServerApplication.java index 1568187a..7c534bf0 100644 --- a/src/main/java/com/stumeet/server/ServerApplication.java +++ b/src/main/java/com/stumeet/server/ServerApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication +@EnableFeignClients public class ServerApplication { public static void main(String[] args) { diff --git a/src/main/java/com/stumeet/server/account/adapter/in/web/AccountController.java b/src/main/java/com/stumeet/server/account/adapter/in/web/AccountController.java deleted file mode 100644 index dd164e07..00000000 --- a/src/main/java/com/stumeet/server/account/adapter/in/web/AccountController.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.stumeet.server.account.adapter.in.web; - -import com.stumeet.server.common.annotation.WebAdapter; - -@WebAdapter -public class AccountController { -} diff --git a/src/main/java/com/stumeet/server/account/adapter/out/persistence/AccountJpaEntity.java b/src/main/java/com/stumeet/server/account/adapter/out/persistence/AccountJpaEntity.java deleted file mode 100644 index 7054e994..00000000 --- a/src/main/java/com/stumeet/server/account/adapter/out/persistence/AccountJpaEntity.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.stumeet.server.account.adapter.out.persistence; - -import jakarta.persistence.Entity; -import jakarta.persistence.Id; - -@Entity -public class AccountJpaEntity { - - @Id - private Long id; -} diff --git a/src/main/java/com/stumeet/server/account/adapter/out/persistence/AccountMapper.java b/src/main/java/com/stumeet/server/account/adapter/out/persistence/AccountMapper.java deleted file mode 100644 index 9d08a283..00000000 --- a/src/main/java/com/stumeet/server/account/adapter/out/persistence/AccountMapper.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.stumeet.server.account.adapter.out.persistence; - -public class AccountMapper { -} diff --git a/src/main/java/com/stumeet/server/account/adapter/out/persistence/AccountPersistenceAdapter.java b/src/main/java/com/stumeet/server/account/adapter/out/persistence/AccountPersistenceAdapter.java deleted file mode 100644 index ca1b8b19..00000000 --- a/src/main/java/com/stumeet/server/account/adapter/out/persistence/AccountPersistenceAdapter.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.stumeet.server.account.adapter.out.persistence; - -import com.stumeet.server.account.application.port.out.AccountPort; -import com.stumeet.server.common.annotation.PersistenceAdapter; - -@PersistenceAdapter -public class AccountPersistenceAdapter implements AccountPort { -} diff --git a/src/main/java/com/stumeet/server/account/adapter/out/persistence/JpaAccountRepository.java b/src/main/java/com/stumeet/server/account/adapter/out/persistence/JpaAccountRepository.java deleted file mode 100644 index 2ad682a1..00000000 --- a/src/main/java/com/stumeet/server/account/adapter/out/persistence/JpaAccountRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.stumeet.server.account.adapter.out.persistence; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface JpaAccountRepository extends JpaRepository { -} diff --git a/src/main/java/com/stumeet/server/account/application/port/in/AccountUseCase.java b/src/main/java/com/stumeet/server/account/application/port/in/AccountUseCase.java deleted file mode 100644 index 1e4db4a6..00000000 --- a/src/main/java/com/stumeet/server/account/application/port/in/AccountUseCase.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.stumeet.server.account.application.port.in; - -public interface AccountUseCase { -} diff --git a/src/main/java/com/stumeet/server/account/application/port/in/SignupCommand.java b/src/main/java/com/stumeet/server/account/application/port/in/SignupCommand.java deleted file mode 100644 index a7ea362b..00000000 --- a/src/main/java/com/stumeet/server/account/application/port/in/SignupCommand.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.stumeet.server.account.application.port.in; - -public record SignupCommand() { -} diff --git a/src/main/java/com/stumeet/server/account/application/port/out/AccountPort.java b/src/main/java/com/stumeet/server/account/application/port/out/AccountPort.java deleted file mode 100644 index 4280dd1f..00000000 --- a/src/main/java/com/stumeet/server/account/application/port/out/AccountPort.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.stumeet.server.account.application.port.out; - -public interface AccountPort { -} diff --git a/src/main/java/com/stumeet/server/account/application/service/AccountService.java b/src/main/java/com/stumeet/server/account/application/service/AccountService.java deleted file mode 100644 index 4ff5a8c3..00000000 --- a/src/main/java/com/stumeet/server/account/application/service/AccountService.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.stumeet.server.account.application.service; - -import com.stumeet.server.account.application.port.in.AccountUseCase; -import com.stumeet.server.common.annotation.UseCase; - -@UseCase -public class AccountService implements AccountUseCase { -} diff --git a/src/main/java/com/stumeet/server/account/domain/Account.java b/src/main/java/com/stumeet/server/account/domain/Account.java deleted file mode 100644 index 4120055b..00000000 --- a/src/main/java/com/stumeet/server/account/domain/Account.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.stumeet.server.account.domain; - -public class Account { -} diff --git a/src/main/java/com/stumeet/server/common/auth/config/AuthConfig.java b/src/main/java/com/stumeet/server/common/auth/config/AuthConfig.java new file mode 100644 index 00000000..9d41df16 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/config/AuthConfig.java @@ -0,0 +1,14 @@ +package com.stumeet.server.common.auth.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; + +@Configuration +public class AuthConfig { + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/stumeet/server/common/auth/config/SecurityConfig.java b/src/main/java/com/stumeet/server/common/auth/config/SecurityConfig.java new file mode 100644 index 00000000..a0db23fe --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/config/SecurityConfig.java @@ -0,0 +1,75 @@ +package com.stumeet.server.common.auth.config; + +import com.stumeet.server.common.auth.filter.JwtAuthenticationFilter; +import com.stumeet.server.common.auth.filter.OAuthAuthenticationFilter; +import com.stumeet.server.common.auth.handler.InvalidAuthenticationFailureHandler; +import com.stumeet.server.common.auth.handler.OAuthAuthenticationSuccessHandler; +import com.stumeet.server.common.auth.service.JwtAuthenticationService; +import com.stumeet.server.common.auth.service.OAuthAuthenticationProvider; +import com.stumeet.server.common.token.JwtTokenProvider; +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.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; + +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + private final InvalidAuthenticationFailureHandler invalidAuthenticationFailureHandler; + private final OAuthAuthenticationProvider authenticationProvider; + private final AuthenticationManager authenticationManager; + private final JwtTokenProvider jwtTokenProvider; + private final JwtAuthenticationService jwtAuthenticationService; + private final OAuthAuthenticationSuccessHandler oAuthAuthenticationSuccessHandler; + private final AuthenticationEntryPoint authenticationEntryPoint; + private final AccessDeniedHandler accessDeniedHandler; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.formLogin(AbstractHttpConfigurer::disable); + http.rememberMe(AbstractHttpConfigurer::disable); + http.httpBasic(AbstractHttpConfigurer::disable); + http.logout(AbstractHttpConfigurer::disable); + http.csrf(AbstractHttpConfigurer::disable); + + http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)); + + http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + http.authenticationProvider(authenticationProvider); + + http.addFilterBefore(new OAuthAuthenticationFilter(invalidAuthenticationFailureHandler, authenticationManager, oAuthAuthenticationSuccessHandler), UsernamePasswordAuthenticationFilter.class); + http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, jwtAuthenticationService), UsernamePasswordAuthenticationFilter.class); + + http.authorizeHttpRequests(auth -> { + auth.requestMatchers("/api/v1/oauth").permitAll(); + auth.requestMatchers("/h2-console/**").permitAll(); + auth.requestMatchers("/api/v1/signup").hasAnyAuthority("FIRST_LOGIN"); + auth.anyRequest().authenticated(); + }); + + http.securityContext(securityContext -> securityContext.securityContextRepository(securityContextRepository())); + + http.exceptionHandling(handler -> { + handler.authenticationEntryPoint(authenticationEntryPoint); + handler.accessDeniedHandler(accessDeniedHandler); + }); + + return http.build(); + } + @Bean + public SecurityContextRepository securityContextRepository() { + return new RequestAttributeSecurityContextRepository(); + } +} diff --git a/src/main/java/com/stumeet/server/common/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/stumeet/server/common/auth/filter/JwtAuthenticationFilter.java new file mode 100644 index 00000000..5ef56bd3 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/filter/JwtAuthenticationFilter.java @@ -0,0 +1,47 @@ +package com.stumeet.server.common.auth.filter; + +import com.stumeet.server.common.auth.model.AuthenticationHeader; +import com.stumeet.server.common.auth.service.JwtAuthenticationService; +import com.stumeet.server.common.token.JwtTokenProvider; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + private final JwtAuthenticationService jwtAuthenticationService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + SecurityContext context = SecurityContextHolder.getContext(); + if (context.getAuthentication() == null) { + String token = resolveToken(request.getHeader(AuthenticationHeader.ACCESS_TOKEN.getName())); + + if (jwtTokenProvider.validateToken(token)) { + Authentication auth = jwtAuthenticationService.getAuthentication(token); + context.setAuthentication(auth); + } + } + + filterChain.doFilter(request, response); + } + + private String resolveToken(String token) { + if (token != null && token.startsWith("Bearer ")) { + return token.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/stumeet/server/common/auth/filter/OAuthAuthenticationFilter.java b/src/main/java/com/stumeet/server/common/auth/filter/OAuthAuthenticationFilter.java new file mode 100644 index 00000000..fb880489 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/filter/OAuthAuthenticationFilter.java @@ -0,0 +1,46 @@ +package com.stumeet.server.common.auth.filter; + +import com.stumeet.server.common.auth.model.AuthenticationHeader; +import com.stumeet.server.common.auth.token.StumeetAuthenticationToken; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import java.io.IOException; + +public class OAuthAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + public OAuthAuthenticationFilter( + AuthenticationFailureHandler failureHandler, + AuthenticationManager authenticationManager, + AuthenticationSuccessHandler authenticationSuccessHandler + ) { + super("/api/v1/oauth"); + setAuthenticationFailureHandler(failureHandler); + setAuthenticationManager(authenticationManager); + setAuthenticationSuccessHandler(authenticationSuccessHandler); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { + String accessToken = request.getHeader(AuthenticationHeader.ACCESS_TOKEN.getName()); + String provider = request.getHeader(AuthenticationHeader.X_OAUTH_PROVIDER.getName()); + + checkAuthorizationInfo(accessToken, provider); + + return getAuthenticationManager().authenticate(StumeetAuthenticationToken.createUnAuthenticationToken(accessToken, provider)); + } + + + private void checkAuthorizationInfo(String accessToken, String provider) { + if (accessToken == null || provider == null) { + throw new BadCredentialsException("잘못된 인증 정보가 전달되었습니다. 확인해주세요"); + } + } +} diff --git a/src/main/java/com/stumeet/server/common/auth/handler/ForbiddenAccessHandler.java b/src/main/java/com/stumeet/server/common/auth/handler/ForbiddenAccessHandler.java new file mode 100644 index 00000000..87c6dd77 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/handler/ForbiddenAccessHandler.java @@ -0,0 +1,32 @@ +package com.stumeet.server.common.auth.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stumeet.server.common.model.ApiResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class ForbiddenAccessHandler implements AccessDeniedHandler { + + private final ObjectMapper objectMapper; + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + log.warn(accessDeniedException.getMessage()); + + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json;charset=utf-8"); + response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.fail(HttpStatus.FORBIDDEN.value(), "유효하지 않은 요청입니다."))); + } +} diff --git a/src/main/java/com/stumeet/server/common/auth/handler/InvalidAuthenticationFailureHandler.java b/src/main/java/com/stumeet/server/common/auth/handler/InvalidAuthenticationFailureHandler.java new file mode 100644 index 00000000..070979c9 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/handler/InvalidAuthenticationFailureHandler.java @@ -0,0 +1,37 @@ +package com.stumeet.server.common.auth.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stumeet.server.common.model.ApiResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class InvalidAuthenticationFailureHandler implements AuthenticationFailureHandler { + private final ObjectMapper objectMapper; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { + log.warn(exception.getMessage()); + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + + ApiResponse apiResponse = ApiResponse.fail( + HttpStatus.UNAUTHORIZED.value(), + "OAuth 인증에 실패했습니다." + ); + + response.getWriter().write(objectMapper.writeValueAsString(apiResponse)); + } +} diff --git a/src/main/java/com/stumeet/server/common/auth/handler/OAuthAuthenticationSuccessHandler.java b/src/main/java/com/stumeet/server/common/auth/handler/OAuthAuthenticationSuccessHandler.java new file mode 100644 index 00000000..d7adfd2c --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/handler/OAuthAuthenticationSuccessHandler.java @@ -0,0 +1,42 @@ +package com.stumeet.server.common.auth.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stumeet.server.common.auth.model.OAuthLoginResponse; +import com.stumeet.server.common.auth.token.StumeetAuthenticationToken; +import com.stumeet.server.common.model.ApiResponse; +import com.stumeet.server.member.domain.UserRole; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class OAuthAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private final ObjectMapper objectMapper; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + StumeetAuthenticationToken token = (StumeetAuthenticationToken) authentication; + boolean isFirstLogin = token.getPrincipal() + .getAuthorities() + .contains(new SimpleGrantedAuthority(UserRole.FIRST_LOGIN.toString())); + + ApiResponse apiResponse = ApiResponse.success( + HttpStatus.OK.value(), + "OAuth 인증에 성공했습니다.", + new OAuthLoginResponse(token.getCredentials(), isFirstLogin) + ); + + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(apiResponse)); + } +} diff --git a/src/main/java/com/stumeet/server/common/auth/handler/UnauthorizedAccessEntryPoint.java b/src/main/java/com/stumeet/server/common/auth/handler/UnauthorizedAccessEntryPoint.java new file mode 100644 index 00000000..9eb6addc --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/handler/UnauthorizedAccessEntryPoint.java @@ -0,0 +1,32 @@ +package com.stumeet.server.common.auth.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stumeet.server.common.model.ApiResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class UnauthorizedAccessEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + log.warn(authException.getMessage()); + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=utf-8"); + response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.fail(HttpStatus.UNAUTHORIZED.value(), "유효하지 않은 사용자입니다. 다시 인증해주세요"))); + } +} diff --git a/src/main/java/com/stumeet/server/common/auth/model/AuthenticationHeader.java b/src/main/java/com/stumeet/server/common/auth/model/AuthenticationHeader.java new file mode 100644 index 00000000..abd87d06 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/model/AuthenticationHeader.java @@ -0,0 +1,13 @@ +package com.stumeet.server.common.auth.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum AuthenticationHeader { + ACCESS_TOKEN("Authorization"), + X_OAUTH_PROVIDER("X-OAUTH-PROVIDER"); + + private final String name; +} diff --git a/src/main/java/com/stumeet/server/common/auth/model/LoginMember.java b/src/main/java/com/stumeet/server/common/auth/model/LoginMember.java new file mode 100644 index 00000000..ec7e8687 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/model/LoginMember.java @@ -0,0 +1,55 @@ +package com.stumeet.server.common.auth.model; + +import com.stumeet.server.member.domain.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +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.List; + +@RequiredArgsConstructor +@Getter +public class LoginMember implements UserDetails { + + private final Member member; + + @Override + public Collection getAuthorities() { + return List.of( + new SimpleGrantedAuthority(member.getRole().toString()) + ); + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return member.getName(); + } + + @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/com/stumeet/server/common/auth/model/OAuthLoginResponse.java b/src/main/java/com/stumeet/server/common/auth/model/OAuthLoginResponse.java new file mode 100644 index 00000000..178d8d60 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/model/OAuthLoginResponse.java @@ -0,0 +1,7 @@ +package com.stumeet.server.common.auth.model; + +public record OAuthLoginResponse( + String accessToken, + boolean isFirstLogin +) { +} diff --git a/src/main/java/com/stumeet/server/common/auth/service/JwtAuthenticationService.java b/src/main/java/com/stumeet/server/common/auth/service/JwtAuthenticationService.java new file mode 100644 index 00000000..f3ad375c --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/service/JwtAuthenticationService.java @@ -0,0 +1,37 @@ +package com.stumeet.server.common.auth.service; + +import com.stumeet.server.common.annotation.UseCase; +import com.stumeet.server.common.auth.model.LoginMember; +import com.stumeet.server.common.auth.token.StumeetAuthenticationToken; +import com.stumeet.server.common.token.JwtTokenProvider; +import com.stumeet.server.member.application.port.in.MemberQueryUseCase; +import com.stumeet.server.member.domain.Member; +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@UseCase +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class JwtAuthenticationService { + + private final JwtTokenProvider jwtTokenProvider; + private final MemberQueryUseCase memberQueryUseCase; + + public StumeetAuthenticationToken getAuthentication(String accessToken) { + Claims claims = jwtTokenProvider.getClaims(accessToken); + String id = claims.getSubject(); + + Member member = memberQueryUseCase.getById(Long.parseLong(id)); + List role = List.of(new SimpleGrantedAuthority(member.getRole().toString())); + + return StumeetAuthenticationToken.createAuthenticationJwtToken( + role, + accessToken, + new LoginMember(member) + ); + } +} diff --git a/src/main/java/com/stumeet/server/common/auth/service/OAuthAuthenticationProvider.java b/src/main/java/com/stumeet/server/common/auth/service/OAuthAuthenticationProvider.java new file mode 100644 index 00000000..6bd06699 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/service/OAuthAuthenticationProvider.java @@ -0,0 +1,56 @@ +package com.stumeet.server.common.auth.service; + +import com.stumeet.server.common.auth.model.LoginMember; +import com.stumeet.server.common.auth.token.StumeetAuthenticationToken; +import com.stumeet.server.common.client.oauth.OAuthClient; +import com.stumeet.server.common.client.oauth.model.OAuthUserProfileResponse; +import com.stumeet.server.common.token.JwtTokenProvider; +import com.stumeet.server.member.application.port.in.MemberOAuthUseCase; +import com.stumeet.server.member.domain.Member; +import feign.FeignException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class OAuthAuthenticationProvider implements AuthenticationProvider { + + private final OAuthClient oAuthClient; + private final MemberOAuthUseCase memberOAuthUseCase; + private final JwtTokenProvider jwtTokenProvider; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + StumeetAuthenticationToken token = (StumeetAuthenticationToken) authentication; + String providerAccessToken = token.getCredentials(); + String provider = token.getProvider(); + + try { + OAuthUserProfileResponse myProfile = oAuthClient.getMyProfile(providerAccessToken); + Member member = memberOAuthUseCase.getMemberOrCreate(myProfile, provider); + LoginMember loginMember = new LoginMember(member); + + String accessToken = jwtTokenProvider.generateToken(loginMember); + + // TODO refresh token 생성 필요 + return StumeetAuthenticationToken.createAuthenticationOAuthToken( + loginMember.getAuthorities(), + accessToken, + provider, + loginMember + ); + } catch (FeignException e) { + throw new BadCredentialsException("OAuth 인증에 실패했습니다.", e); + } + } + + @Override + public boolean supports(Class authentication) { + return StumeetAuthenticationToken.class.isAssignableFrom(authentication); + } + +} diff --git a/src/main/java/com/stumeet/server/common/auth/token/StumeetAuthenticationToken.java b/src/main/java/com/stumeet/server/common/auth/token/StumeetAuthenticationToken.java new file mode 100644 index 00000000..f7f72959 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/auth/token/StumeetAuthenticationToken.java @@ -0,0 +1,63 @@ +package com.stumeet.server.common.auth.token; + +import com.stumeet.server.common.auth.model.LoginMember; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +public class StumeetAuthenticationToken extends AbstractAuthenticationToken { + private final LoginMember principal; + private final String accessToken; + private final String provider; + + private StumeetAuthenticationToken(Collection authorities, String accessToken, String provider, LoginMember principal, boolean isAuthenticated) { + super(authorities); + this.principal = principal; + this.accessToken = accessToken; + this.provider = provider; + super.setAuthenticated(isAuthenticated); + } + + public static StumeetAuthenticationToken createUnAuthenticationToken(String accessToken, String provider) { + return new StumeetAuthenticationToken(List.of(), accessToken, provider, null, false); + } + + public static StumeetAuthenticationToken createAuthenticationOAuthToken(Collection authorities, String accessToken, String provider, LoginMember principal) { + return new StumeetAuthenticationToken(authorities, accessToken, provider, principal, true); + } + + public static StumeetAuthenticationToken createAuthenticationJwtToken(Collection authorities, String accessToken, LoginMember principal) { + return new StumeetAuthenticationToken(authorities, accessToken, null, principal, true); + } + + @Override + public String getCredentials() { + return accessToken; + } + + @Override + public LoginMember getPrincipal() { + return principal; + } + + public String getProvider() { + return provider; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + StumeetAuthenticationToken that = (StumeetAuthenticationToken) o; + return Objects.equals(principal, that.principal) && Objects.equals(accessToken, that.accessToken) && Objects.equals(provider, that.provider); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), principal, accessToken, provider); + } +} diff --git a/src/main/java/com/stumeet/server/common/client/oauth/OAuthClient.java b/src/main/java/com/stumeet/server/common/client/oauth/OAuthClient.java new file mode 100644 index 00000000..054b7d9f --- /dev/null +++ b/src/main/java/com/stumeet/server/common/client/oauth/OAuthClient.java @@ -0,0 +1,7 @@ +package com.stumeet.server.common.client.oauth; + +import com.stumeet.server.common.client.oauth.model.OAuthUserProfileResponse; + +public interface OAuthClient { + OAuthUserProfileResponse getMyProfile(String accessToken); +} diff --git a/src/main/java/com/stumeet/server/common/client/oauth/kakao/KakaoOAuthClient.java b/src/main/java/com/stumeet/server/common/client/oauth/kakao/KakaoOAuthClient.java new file mode 100644 index 00000000..85751810 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/client/oauth/kakao/KakaoOAuthClient.java @@ -0,0 +1,29 @@ +package com.stumeet.server.common.client.oauth.kakao; + +import com.stumeet.server.common.client.oauth.OAuthClient; +import com.stumeet.server.common.client.oauth.kakao.model.KakaoUserProfileResponse; +import com.stumeet.server.common.client.oauth.model.OAuthUserProfileResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class KakaoOAuthClient implements OAuthClient { + + private final KakaoOAuthFeignClient kakaoOAuthClient; + + @Override + public OAuthUserProfileResponse getMyProfile(String accessToken) { + String propertyKey = "property_keys=[\"kakao_account.profile\"]"; + ResponseEntity response = kakaoOAuthClient.getMyProfile(accessToken, propertyKey); + + KakaoUserProfileResponse responseBody = response.getBody(); + + return new OAuthUserProfileResponse( + responseBody.id(), + responseBody.kakaoAccount().profile().nickname(), + responseBody.kakaoAccount().profile().thumbnailImageUrl() + ); + } +} diff --git a/src/main/java/com/stumeet/server/common/client/oauth/kakao/KakaoOAuthFeignClient.java b/src/main/java/com/stumeet/server/common/client/oauth/kakao/KakaoOAuthFeignClient.java new file mode 100644 index 00000000..78dc2bf3 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/client/oauth/kakao/KakaoOAuthFeignClient.java @@ -0,0 +1,20 @@ +package com.stumeet.server.common.client.oauth.kakao; + +import com.stumeet.server.common.client.oauth.kakao.model.KakaoUserProfileResponse; +import feign.Headers; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient(name = "kakaoOAuthClient", url = "https://kapi.kakao.com") +public interface KakaoOAuthFeignClient { + + @PostMapping("/v2/user/me") + @Headers("Content-Type: application/x-www-form-urlencoded") + ResponseEntity getMyProfile( + @RequestHeader("Authorization") String accessToken, + @RequestBody String propertyKey + ); +} diff --git a/src/main/java/com/stumeet/server/common/client/oauth/kakao/model/KakaoUserProfileResponse.java b/src/main/java/com/stumeet/server/common/client/oauth/kakao/model/KakaoUserProfileResponse.java new file mode 100644 index 00000000..33f38073 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/client/oauth/kakao/model/KakaoUserProfileResponse.java @@ -0,0 +1,20 @@ +package com.stumeet.server.common.client.oauth.kakao.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record KakaoUserProfileResponse( + String id, + @JsonProperty("kakao_account") + KakaoAccount kakaoAccount +) { + public record KakaoAccount( + Profile profile + ) { + public record Profile( + String nickname, + @JsonProperty("thumbnail_image_url") + String thumbnailImageUrl + ) { + } + } +} diff --git a/src/main/java/com/stumeet/server/common/client/oauth/model/OAuthUserProfileResponse.java b/src/main/java/com/stumeet/server/common/client/oauth/model/OAuthUserProfileResponse.java new file mode 100644 index 00000000..84bbc69d --- /dev/null +++ b/src/main/java/com/stumeet/server/common/client/oauth/model/OAuthUserProfileResponse.java @@ -0,0 +1,8 @@ +package com.stumeet.server.common.client.oauth.model; + +public record OAuthUserProfileResponse( + String id, + String name, + String imageUrl +) { +} diff --git a/src/main/java/com/stumeet/server/common/config/AuditConfig.java b/src/main/java/com/stumeet/server/common/config/AuditConfig.java new file mode 100644 index 00000000..d9fbede0 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/config/AuditConfig.java @@ -0,0 +1,9 @@ +package com.stumeet.server.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class AuditConfig { +} diff --git a/src/main/java/com/stumeet/server/common/config/QuerydslConfig.java b/src/main/java/com/stumeet/server/common/config/QuerydslConfig.java new file mode 100644 index 00000000..ab781f50 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/config/QuerydslConfig.java @@ -0,0 +1,19 @@ +package com.stumeet.server.common.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class QuerydslConfig { + + private final EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/com/stumeet/server/common/model/ApiResponse.java b/src/main/java/com/stumeet/server/common/model/ApiResponse.java new file mode 100644 index 00000000..18a49786 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/model/ApiResponse.java @@ -0,0 +1,22 @@ +package com.stumeet.server.common.model; + +import com.fasterxml.jackson.annotation.JsonInclude; + +public record ApiResponse( + int code, + String message, + @JsonInclude(value = JsonInclude.Include.NON_NULL) + T data +) { + public static ApiResponse success(int code, String message, T data) { + return new ApiResponse<>(code, message, data); + } + + public static ApiResponse success(int code, String message) { + return new ApiResponse<>(code, message, null); + } + + public static ApiResponse fail(int code, String message) { + return new ApiResponse<>(code, message, null); + } +} diff --git a/src/main/java/com/stumeet/server/common/model/BaseTimeEntity.java b/src/main/java/com/stumeet/server/common/model/BaseTimeEntity.java new file mode 100644 index 00000000..5fa73447 --- /dev/null +++ b/src/main/java/com/stumeet/server/common/model/BaseTimeEntity.java @@ -0,0 +1,29 @@ +package com.stumeet.server.common.model; + + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.hibernate.annotations.Comment; +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 BaseTimeEntity { + + @CreatedDate + @Column(name = "created_at", updatable = false) + @Comment("생성 시간") + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + @Comment("수정 시간") + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/stumeet/server/common/token/JwtTokenProvider.java b/src/main/java/com/stumeet/server/common/token/JwtTokenProvider.java new file mode 100644 index 00000000..7b63fa3b --- /dev/null +++ b/src/main/java/com/stumeet/server/common/token/JwtTokenProvider.java @@ -0,0 +1,82 @@ +package com.stumeet.server.common.token; + +import com.stumeet.server.common.auth.model.LoginMember; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.stream.Collectors; + +@Component +@Slf4j +public class JwtTokenProvider { + private static final String AUTHORITIES_KEY = "auth"; + + private final String issuer; + private final String secret; + + private final long tokenValidityTime; + private final SecretKey secretKey; + + public JwtTokenProvider( + @Value("${jwt.issuer}") String issuer, + @Value("${jwt.secret}") String secret, + @Value("${jwt.validate-time}") long tokenValidityTime + ) { + this.issuer = issuer; + this.secret = secret; + this.tokenValidityTime = tokenValidityTime; + this.secretKey = createSecretKey(secret); + } + + private SecretKey createSecretKey(String secret) { + byte[] keyBytes = Decoders.BASE64.decode(secret); + return Keys.hmacShaKeyFor(keyBytes); + } + + public String generateToken(LoginMember member) { + String authorities = member.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + long now = new Date().getTime(); + Date validityTime = new Date(now + tokenValidityTime); + + return Jwts.builder() + .issuer(issuer) + .subject(String.valueOf(member.getMember().getId())) + .claim(AUTHORITIES_KEY, authorities) + .signWith(secretKey) + .expiration(validityTime) + .compact(); + + } + + public boolean validateToken(String token) { + try { + Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token); + return true; + } catch (JwtException e) { + log.warn("신뢰할 수 없는 JWT 토큰 입니다.", e); + } + return false; + } + + public Claims getClaims(String token) { + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} diff --git a/src/main/java/com/stumeet/server/member/adapter/in/web/MemberSignupApi.java b/src/main/java/com/stumeet/server/member/adapter/in/web/MemberSignupApi.java new file mode 100644 index 00000000..56d00533 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/adapter/in/web/MemberSignupApi.java @@ -0,0 +1,36 @@ +package com.stumeet.server.member.adapter.in.web; + +import com.stumeet.server.common.annotation.WebAdapter; +import com.stumeet.server.common.auth.model.LoginMember; +import com.stumeet.server.common.model.ApiResponse; +import com.stumeet.server.member.application.port.in.MemberSignupCommand; +import com.stumeet.server.member.application.port.in.MemberSignupUseCase; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +@WebAdapter +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class MemberSignupApi { + + private final MemberSignupUseCase memberSignupUseCase; + + @PostMapping("/signup") + public ResponseEntity> signup( + @AuthenticationPrincipal LoginMember loginMember, + @RequestBody @Valid MemberSignupCommand request + ) { + memberSignupUseCase.signup(loginMember.getMember(), request); + + return new ResponseEntity<>( + ApiResponse.success(HttpStatus.CREATED.value(), "회원가입에 성공했습니다."), + HttpStatus.CREATED + ); + } +} diff --git a/src/main/java/com/stumeet/server/member/adapter/out/persistence/JpaMemberRepository.java b/src/main/java/com/stumeet/server/member/adapter/out/persistence/JpaMemberRepository.java new file mode 100644 index 00000000..ea3f0619 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/adapter/out/persistence/JpaMemberRepository.java @@ -0,0 +1,6 @@ +package com.stumeet.server.member.adapter.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaMemberRepository extends JpaRepository, JpaMemberRepositoryCustom { +} diff --git a/src/main/java/com/stumeet/server/member/adapter/out/persistence/JpaMemberRepositoryCustom.java b/src/main/java/com/stumeet/server/member/adapter/out/persistence/JpaMemberRepositoryCustom.java new file mode 100644 index 00000000..1c462e19 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/adapter/out/persistence/JpaMemberRepositoryCustom.java @@ -0,0 +1,7 @@ +package com.stumeet.server.member.adapter.out.persistence; + +import com.stumeet.server.member.domain.OAuthProvider; + +public interface JpaMemberRepositoryCustom { + MemberJpaEntity getByOAuthProviderId(String oAuthProviderId, OAuthProvider provider); +} diff --git a/src/main/java/com/stumeet/server/member/adapter/out/persistence/JpaMemberRepositoryCustomImpl.java b/src/main/java/com/stumeet/server/member/adapter/out/persistence/JpaMemberRepositoryCustomImpl.java new file mode 100644 index 00000000..6a50334e --- /dev/null +++ b/src/main/java/com/stumeet/server/member/adapter/out/persistence/JpaMemberRepositoryCustomImpl.java @@ -0,0 +1,28 @@ +package com.stumeet.server.member.adapter.out.persistence; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.stumeet.server.member.domain.OAuthProvider; +import lombok.RequiredArgsConstructor; + +import static com.stumeet.server.member.adapter.out.persistence.QMemberJpaEntity.memberJpaEntity; +import static com.stumeet.server.member.adapter.out.persistence.QOAuthLoginJpaEntity.oAuthLoginJpaEntity; + +@RequiredArgsConstructor +public class JpaMemberRepositoryCustomImpl implements JpaMemberRepositoryCustom { + + private final JPAQueryFactory query; + + @Override + public MemberJpaEntity getByOAuthProviderId(String oAuthProviderId, OAuthProvider provider) { + return query + .select(memberJpaEntity) + .from(memberJpaEntity) + .join(oAuthLoginJpaEntity) + .on(oAuthLoginJpaEntity.member.id.eq(memberJpaEntity.id)) + .where( + oAuthLoginJpaEntity.providerId.eq(oAuthProviderId), + oAuthLoginJpaEntity.providerName.eq(provider) + ) + .fetchOne(); + } +} diff --git a/src/main/java/com/stumeet/server/member/adapter/out/persistence/JpaOAuthLoginRepository.java b/src/main/java/com/stumeet/server/member/adapter/out/persistence/JpaOAuthLoginRepository.java new file mode 100644 index 00000000..b63c3d89 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/adapter/out/persistence/JpaOAuthLoginRepository.java @@ -0,0 +1,7 @@ +package com.stumeet.server.member.adapter.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaOAuthLoginRepository extends JpaRepository { + boolean existsOAuthLoginJpaEntityByProviderId(String providerId); +} diff --git a/src/main/java/com/stumeet/server/member/adapter/out/persistence/MemberJpaEntity.java b/src/main/java/com/stumeet/server/member/adapter/out/persistence/MemberJpaEntity.java new file mode 100644 index 00000000..33f90886 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/adapter/out/persistence/MemberJpaEntity.java @@ -0,0 +1,63 @@ +package com.stumeet.server.member.adapter.out.persistence; + +import com.stumeet.server.common.model.BaseTimeEntity; +import com.stumeet.server.member.domain.AuthType; +import com.stumeet.server.member.domain.UserRole; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.Comment; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "member") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Getter +public class MemberJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("멤버 아이디") + private Long id; + + @Column(name = "name", nullable = false) + @Comment("멤버 이름") + private String name; + + @Column(name = "image", nullable = false) + @Comment("멤버 이미지 URL") + private String image; + + @Column(name = "sugar_contents", nullable = false) + @Comment("포도알 당도") + private Double sugarContents; + + @Column(name = "region", length = 50) + @Comment("지역") + private String region; + + @Column(name = "profession", length = 50) + @Comment("분야") + private String profession; + + @Column(name = "auth_type", length = 50, nullable = false) + @Enumerated(EnumType.STRING) + @Comment("인증 방법(OAuth, 자체 로그인 등)") + private AuthType authType; + + @Column(name = "role", length = 20, nullable = false) + @Enumerated(EnumType.STRING) + @Comment("권한") + private UserRole role; + + @Column(name = "is_deleted", nullable = false) + @Comment("삭제 여부") + private boolean isDeleted; + + @Column(name = "deleted_at") + @Comment("삭제된 시간") + private LocalDateTime deletedAt; + +} diff --git a/src/main/java/com/stumeet/server/member/adapter/out/persistence/MemberPersistenceAdapter.java b/src/main/java/com/stumeet/server/member/adapter/out/persistence/MemberPersistenceAdapter.java new file mode 100644 index 00000000..5f58f30b --- /dev/null +++ b/src/main/java/com/stumeet/server/member/adapter/out/persistence/MemberPersistenceAdapter.java @@ -0,0 +1,38 @@ +package com.stumeet.server.member.adapter.out.persistence; + +import com.stumeet.server.member.application.port.out.MemberCommandPort; +import com.stumeet.server.member.application.port.out.MemberQueryPort; +import com.stumeet.server.common.annotation.PersistenceAdapter; +import com.stumeet.server.member.domain.Member; +import com.stumeet.server.member.domain.OAuthProvider; +import lombok.RequiredArgsConstructor; + +@PersistenceAdapter +@RequiredArgsConstructor +public class MemberPersistenceAdapter implements MemberQueryPort, MemberCommandPort { + + private final JpaMemberRepository jpaMemberRepository; + private final MemberPersistenceMapper memberMapper; + + + @Override + public Member save(Member member) { + MemberJpaEntity entity = jpaMemberRepository.save(memberMapper.toEntity(member)); + return memberMapper.toDomain(entity); + } + + @Override + public Member getByOAuthProviderId(String oAuthProviderId, OAuthProvider provider) { + return memberMapper.toDomain( + jpaMemberRepository.getByOAuthProviderId(oAuthProviderId, provider) + ); + } + + @Override + public Member getById(Long id) { + MemberJpaEntity memberJpaEntity = jpaMemberRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("해당하는 ID의 유저가 존재하지 않습니다.")); + + return memberMapper.toDomain(memberJpaEntity); + } +} diff --git a/src/main/java/com/stumeet/server/member/adapter/out/persistence/MemberPersistenceMapper.java b/src/main/java/com/stumeet/server/member/adapter/out/persistence/MemberPersistenceMapper.java new file mode 100644 index 00000000..20064ef2 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/adapter/out/persistence/MemberPersistenceMapper.java @@ -0,0 +1,34 @@ +package com.stumeet.server.member.adapter.out.persistence; + +import com.stumeet.server.member.domain.Member; +import org.springframework.stereotype.Component; + +@Component +public class MemberPersistenceMapper { + + public MemberJpaEntity toEntity(Member domain) { + return MemberJpaEntity.builder() + .id(domain.getId()) + .name(domain.getName()) + .image(domain.getImage()) + .sugarContents(domain.getSugarContents()) + .region(domain.getRegion()) + .profession(domain.getProfession()) + .authType(domain.getAuthType()) + .role(domain.getRole()) + .build(); + } + + public Member toDomain(MemberJpaEntity entity) { + return Member.builder() + .id(entity.getId()) + .name(entity.getName()) + .image(entity.getImage()) + .sugarContents(entity.getSugarContents()) + .region(entity.getRegion()) + .profession(entity.getProfession()) + .authType(entity.getAuthType()) + .role(entity.getRole()) + .build(); + } +} diff --git a/src/main/java/com/stumeet/server/member/adapter/out/persistence/OAuthLoginJpaEntity.java b/src/main/java/com/stumeet/server/member/adapter/out/persistence/OAuthLoginJpaEntity.java new file mode 100644 index 00000000..015ce4ca --- /dev/null +++ b/src/main/java/com/stumeet/server/member/adapter/out/persistence/OAuthLoginJpaEntity.java @@ -0,0 +1,35 @@ +package com.stumeet.server.member.adapter.out.persistence; + +import com.stumeet.server.common.model.BaseTimeEntity; +import com.stumeet.server.member.domain.OAuthProvider; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.Comment; + +@Entity +@Table(name = "oauth_login") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Getter +public class OAuthLoginJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("OAuth 로그인 정보 ID") + private Long id; + + @OneToOne + @JoinColumn(name = "member_id") + @Comment("멤버 ID") + private MemberJpaEntity member; + + @Column(name = "provider_name", length = 50, nullable = false) + @Enumerated(EnumType.STRING) + @Comment("제공자 이름") + private OAuthProvider providerName; + + @Column(name = "provider_id", length = 50, nullable = false) + @Comment("OAuth 사용자 고유 아이디") + private String providerId; +} diff --git a/src/main/java/com/stumeet/server/member/adapter/out/persistence/OAuthLoginPersistenceAdapter.java b/src/main/java/com/stumeet/server/member/adapter/out/persistence/OAuthLoginPersistenceAdapter.java new file mode 100644 index 00000000..a47e01ac --- /dev/null +++ b/src/main/java/com/stumeet/server/member/adapter/out/persistence/OAuthLoginPersistenceAdapter.java @@ -0,0 +1,26 @@ +package com.stumeet.server.member.adapter.out.persistence; + +import com.stumeet.server.common.annotation.PersistenceAdapter; +import com.stumeet.server.member.application.port.out.OAuthLoginCommandPort; +import com.stumeet.server.member.application.port.out.OAuthLoginQueryPort; +import com.stumeet.server.member.domain.OAuthLogin; +import lombok.RequiredArgsConstructor; + +@PersistenceAdapter +@RequiredArgsConstructor +public class OAuthLoginPersistenceAdapter implements OAuthLoginQueryPort, OAuthLoginCommandPort { + + private final JpaOAuthLoginRepository jpaOAuthLoginRepository; + private final OAuthLoginPersistenceMapper oAuthLoginMapper; + + public boolean existsByProviderId(String providerId) { + return jpaOAuthLoginRepository.existsOAuthLoginJpaEntityByProviderId(providerId); + } + + @Override + public OAuthLogin save(OAuthLogin oAuthLogin) { + OAuthLoginJpaEntity entity = jpaOAuthLoginRepository.save(oAuthLoginMapper.toEntity(oAuthLogin)); + + return oAuthLoginMapper.toDomain(entity); + } +} diff --git a/src/main/java/com/stumeet/server/member/adapter/out/persistence/OAuthLoginPersistenceMapper.java b/src/main/java/com/stumeet/server/member/adapter/out/persistence/OAuthLoginPersistenceMapper.java new file mode 100644 index 00000000..c0170bbd --- /dev/null +++ b/src/main/java/com/stumeet/server/member/adapter/out/persistence/OAuthLoginPersistenceMapper.java @@ -0,0 +1,31 @@ +package com.stumeet.server.member.adapter.out.persistence; + +import com.stumeet.server.member.domain.Member; +import com.stumeet.server.member.domain.OAuthLogin; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class OAuthLoginPersistenceMapper { + + private final MemberPersistenceMapper memberPersistenceMapper; + + public OAuthLoginJpaEntity toEntity(OAuthLogin domain) { + return OAuthLoginJpaEntity.builder() + .id(domain.getId()) + .member(memberPersistenceMapper.toEntity(domain.getMember())) + .providerName(domain.getProviderName()) + .providerId(domain.getProviderId()) + .build(); + } + + public OAuthLogin toDomain(OAuthLoginJpaEntity entity) { + return OAuthLogin.builder() + .id(entity.getId()) + .member(memberPersistenceMapper.toDomain(entity.getMember())) + .providerName(entity.getProviderName()) + .providerId(entity.getProviderId()) + .build(); + } +} diff --git a/src/main/java/com/stumeet/server/member/application/port/in/MemberOAuthUseCase.java b/src/main/java/com/stumeet/server/member/application/port/in/MemberOAuthUseCase.java new file mode 100644 index 00000000..9e865379 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/application/port/in/MemberOAuthUseCase.java @@ -0,0 +1,8 @@ +package com.stumeet.server.member.application.port.in; + +import com.stumeet.server.common.client.oauth.model.OAuthUserProfileResponse; +import com.stumeet.server.member.domain.Member; + +public interface MemberOAuthUseCase { + Member getMemberOrCreate(OAuthUserProfileResponse response, String provider); +} diff --git a/src/main/java/com/stumeet/server/member/application/port/in/MemberQueryUseCase.java b/src/main/java/com/stumeet/server/member/application/port/in/MemberQueryUseCase.java new file mode 100644 index 00000000..01a9562f --- /dev/null +++ b/src/main/java/com/stumeet/server/member/application/port/in/MemberQueryUseCase.java @@ -0,0 +1,8 @@ +package com.stumeet.server.member.application.port.in; + +import com.stumeet.server.member.domain.Member; + +public interface MemberQueryUseCase { + + Member getById(Long id); +} diff --git a/src/main/java/com/stumeet/server/member/application/port/in/MemberSignupCommand.java b/src/main/java/com/stumeet/server/member/application/port/in/MemberSignupCommand.java new file mode 100644 index 00000000..aa14bef2 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/application/port/in/MemberSignupCommand.java @@ -0,0 +1,12 @@ +package com.stumeet.server.member.application.port.in; + +import jakarta.validation.constraints.NotBlank; + +public record MemberSignupCommand( + @NotBlank(message = "지역을 입력해주세요") + String region, + + @NotBlank(message = "분야를 선택해주세요") + String profession +) { +} diff --git a/src/main/java/com/stumeet/server/member/application/port/in/MemberSignupUseCase.java b/src/main/java/com/stumeet/server/member/application/port/in/MemberSignupUseCase.java new file mode 100644 index 00000000..38290ae7 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/application/port/in/MemberSignupUseCase.java @@ -0,0 +1,8 @@ +package com.stumeet.server.member.application.port.in; + +import com.stumeet.server.member.domain.Member; + +public interface MemberSignupUseCase { + + void signup(Member member, MemberSignupCommand request); +} diff --git a/src/main/java/com/stumeet/server/member/application/port/out/MemberCommandPort.java b/src/main/java/com/stumeet/server/member/application/port/out/MemberCommandPort.java new file mode 100644 index 00000000..f7c91e97 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/application/port/out/MemberCommandPort.java @@ -0,0 +1,8 @@ +package com.stumeet.server.member.application.port.out; + +import com.stumeet.server.member.domain.Member; + +public interface MemberCommandPort { + + Member save(Member member); +} diff --git a/src/main/java/com/stumeet/server/member/application/port/out/MemberQueryPort.java b/src/main/java/com/stumeet/server/member/application/port/out/MemberQueryPort.java new file mode 100644 index 00000000..4b15f8e4 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/application/port/out/MemberQueryPort.java @@ -0,0 +1,11 @@ +package com.stumeet.server.member.application.port.out; + +import com.stumeet.server.member.domain.Member; +import com.stumeet.server.member.domain.OAuthProvider; + +public interface MemberQueryPort { + Member getByOAuthProviderId(String oAuthProviderId, OAuthProvider provider); + + Member getById(Long id); + +} diff --git a/src/main/java/com/stumeet/server/member/application/port/out/OAuthLoginCommandPort.java b/src/main/java/com/stumeet/server/member/application/port/out/OAuthLoginCommandPort.java new file mode 100644 index 00000000..87ab8b72 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/application/port/out/OAuthLoginCommandPort.java @@ -0,0 +1,7 @@ +package com.stumeet.server.member.application.port.out; + +import com.stumeet.server.member.domain.OAuthLogin; + +public interface OAuthLoginCommandPort { + OAuthLogin save(OAuthLogin oAuthLogin); +} diff --git a/src/main/java/com/stumeet/server/member/application/port/out/OAuthLoginQueryPort.java b/src/main/java/com/stumeet/server/member/application/port/out/OAuthLoginQueryPort.java new file mode 100644 index 00000000..b9c46fbc --- /dev/null +++ b/src/main/java/com/stumeet/server/member/application/port/out/OAuthLoginQueryPort.java @@ -0,0 +1,5 @@ +package com.stumeet.server.member.application.port.out; + +public interface OAuthLoginQueryPort { + boolean existsByProviderId(String providerId); +} diff --git a/src/main/java/com/stumeet/server/member/application/service/MemberOAuthService.java b/src/main/java/com/stumeet/server/member/application/service/MemberOAuthService.java new file mode 100644 index 00000000..e5e7254d --- /dev/null +++ b/src/main/java/com/stumeet/server/member/application/service/MemberOAuthService.java @@ -0,0 +1,50 @@ +package com.stumeet.server.member.application.service; + +import com.stumeet.server.common.annotation.UseCase; +import com.stumeet.server.common.client.oauth.model.OAuthUserProfileResponse; +import com.stumeet.server.member.application.port.in.MemberOAuthUseCase; +import com.stumeet.server.member.application.port.out.MemberCommandPort; +import com.stumeet.server.member.application.port.out.MemberQueryPort; +import com.stumeet.server.member.application.port.out.OAuthLoginCommandPort; +import com.stumeet.server.member.application.port.out.OAuthLoginQueryPort; +import com.stumeet.server.member.domain.*; +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +public class MemberOAuthService implements MemberOAuthUseCase { + + private final MemberCommandPort memberCommandPort; + private final MemberQueryPort memberQueryPort; + private final OAuthLoginQueryPort oAuthLoginQueryPort; + private final OAuthLoginCommandPort oAuthLoginCommandPort; + + @Override + public Member getMemberOrCreate(OAuthUserProfileResponse response, String provider) { + boolean isRegisterUser = oAuthLoginQueryPort.existsByProviderId(response.id()); + OAuthProvider oAuthProvider = OAuthProvider.findByProvider(provider); + Member member; + + if (isRegisterUser) { + member = memberQueryPort.getByOAuthProviderId(response.id(), oAuthProvider); + } else { + member = memberCommandPort.save( + Member.builder() + .name(response.name()) + .image(response.imageUrl()) + .sugarContents(0.0) + .authType(AuthType.OAUTH) + .role(UserRole.FIRST_LOGIN) + .build() + ); + oAuthLoginCommandPort.save( + OAuthLogin.builder() + .member(member) + .providerName(oAuthProvider) + .providerId(response.id()) + .build() + ); + } + return member; + } +} diff --git a/src/main/java/com/stumeet/server/member/application/service/MemberQueryService.java b/src/main/java/com/stumeet/server/member/application/service/MemberQueryService.java new file mode 100644 index 00000000..67223dc3 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/application/service/MemberQueryService.java @@ -0,0 +1,21 @@ +package com.stumeet.server.member.application.service; + +import com.stumeet.server.common.annotation.UseCase; +import com.stumeet.server.member.application.port.in.MemberQueryUseCase; +import com.stumeet.server.member.application.port.out.MemberQueryPort; +import com.stumeet.server.member.domain.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class MemberQueryService implements MemberQueryUseCase { + + private final MemberQueryPort memberQueryPort; + + @Override + public Member getById(Long id) { + return memberQueryPort.getById(id); + } +} diff --git a/src/main/java/com/stumeet/server/member/application/service/MemberSignupService.java b/src/main/java/com/stumeet/server/member/application/service/MemberSignupService.java new file mode 100644 index 00000000..b79d316d --- /dev/null +++ b/src/main/java/com/stumeet/server/member/application/service/MemberSignupService.java @@ -0,0 +1,25 @@ +package com.stumeet.server.member.application.service; + +import com.stumeet.server.common.annotation.UseCase; +import com.stumeet.server.member.application.port.in.MemberSignupCommand; +import com.stumeet.server.member.application.port.in.MemberSignupUseCase; +import com.stumeet.server.member.application.port.out.MemberCommandPort; +import com.stumeet.server.member.domain.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@Transactional +@RequiredArgsConstructor +public class MemberSignupService implements MemberSignupUseCase { + + private final MemberCommandPort memberCommandPort; + + @Override + public void signup(Member member, MemberSignupCommand request) { + member.registerWithAdditionalDetails(request); + + memberCommandPort.save(member); + } + +} diff --git a/src/main/java/com/stumeet/server/member/domain/AuthType.java b/src/main/java/com/stumeet/server/member/domain/AuthType.java new file mode 100644 index 00000000..e82b5533 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/domain/AuthType.java @@ -0,0 +1,5 @@ +package com.stumeet.server.member.domain; + +public enum AuthType { + OAUTH +} diff --git a/src/main/java/com/stumeet/server/member/domain/Member.java b/src/main/java/com/stumeet/server/member/domain/Member.java new file mode 100644 index 00000000..e3aa4d95 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/domain/Member.java @@ -0,0 +1,31 @@ +package com.stumeet.server.member.domain; + +import com.stumeet.server.member.application.port.in.MemberSignupCommand; +import lombok.*; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Getter +public class Member { + private Long id; + + private String name; + + private String image; + + private Double sugarContents; + + private String region; + + private String profession; + + private AuthType authType; + + private UserRole role; + + public void registerWithAdditionalDetails(MemberSignupCommand request) { + this.region = request.region(); + this.profession = request.profession(); + this.role = UserRole.MEMBER; + } +} diff --git a/src/main/java/com/stumeet/server/member/domain/OAuthLogin.java b/src/main/java/com/stumeet/server/member/domain/OAuthLogin.java new file mode 100644 index 00000000..67c35442 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/domain/OAuthLogin.java @@ -0,0 +1,19 @@ +package com.stumeet.server.member.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Getter +public class OAuthLogin { + private Long id; + + private Member member; + + private OAuthProvider providerName; + + private String providerId; +} diff --git a/src/main/java/com/stumeet/server/member/domain/OAuthProvider.java b/src/main/java/com/stumeet/server/member/domain/OAuthProvider.java new file mode 100644 index 00000000..23b0f29c --- /dev/null +++ b/src/main/java/com/stumeet/server/member/domain/OAuthProvider.java @@ -0,0 +1,15 @@ +package com.stumeet.server.member.domain; + +import java.util.Arrays; + +public enum OAuthProvider { + KAKAO, + APPLE; + + public static OAuthProvider findByProvider(String provider) { + return Arrays.stream(values()) + .filter(p -> p.toString().toLowerCase().equals(provider)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("알 수 없는 OAuth provider 입니다.")); + } +} diff --git a/src/main/java/com/stumeet/server/member/domain/UserRole.java b/src/main/java/com/stumeet/server/member/domain/UserRole.java new file mode 100644 index 00000000..d98d28a2 --- /dev/null +++ b/src/main/java/com/stumeet/server/member/domain/UserRole.java @@ -0,0 +1,6 @@ +package com.stumeet.server.member.domain; + +public enum UserRole { + MEMBER, + FIRST_LOGIN +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 00000000..f9fa78db --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,29 @@ +spring: + config: + activate: + on-profile: local + import: application-secret.properties + datasource: + url: jdbc:h2:tcp://localhost/~/test;DATABASE_TO_UPPER=false;MODE=MYSQL + username: sa + password: + driver-class-name: org.h2.Driver + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + show_sql: true + format_sql: true + +jwt: + issuer: ${JWT_ISSUER} + secret: ${JWT_SECRET} + validate-time: ${ACCESS_TOKEN_EXPIRED_TIME} + +logging: + level: + org: + springframework: + security: DEBUG \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..1da87e07 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,25 @@ +server: + port: 8080 + +spring: + config: + activate: + on-profile: prod + import: application-secret.properties + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${DEV_DB_ENDPOINT_URL}:3306/${DEV_DB_NAME}?serverTimezone=Asia/Seoul&autoReconnect=true&useSSL=true&useUnicode=true + username: ${DEV_DB_USER} + password: ${DEV_DB_PASSWORD} + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + show_sql: true + dialect: org.hibernate.dialect.MySQLDialect + +jwt: + issuer: ${JWT_ISSUER} + secret: ${JWT_SECRET} + validate-time: ${ACCESS_TOKEN_EXPIRED_TIME} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b137891..00000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -