diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..f05219c --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,80 @@ +name: Deploy (dev) to EC2 + +on: + push: + branches: ["dev"] + +jobs: + deploy: + runs-on: ubuntu-latest + + env: + WORKDIR: . + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + cache: gradle + + - name: Build (Gradle) + working-directory: ${{ env.WORKDIR }} + run: | + chmod +x ./gradlew + ./gradlew clean build -x test + + - name: Pick jar + working-directory: ${{ env.WORKDIR }} + run: | + ls -al build/libs + + JAR_PATH="$(ls build/libs/*.jar | grep -v -E '(plain|original)\.jar$' | head -n 1)" + if [ -z "$JAR_PATH" ]; then + echo "No runnable jar found in build/libs" + ls -al build/libs + exit 1 + fi + echo "JAR_PATH=$JAR_PATH" >> $GITHUB_ENV + echo "Selected: $JAR_PATH" + + - name: Configure SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H "${{ secrets.EC2_HOST }}" >> ~/.ssh/known_hosts + + - name: Upload jar to EC2 + run: | + scp -i ~/.ssh/id_rsa \ + "$JAR_PATH" \ + "${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }}:/opt/member/app.jar" + + - name: Restart service + run: | + ssh -i ~/.ssh/id_rsa "${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }}" \ + "sudo systemctl restart member && sudo systemctl status member --no-pager -l" + + - name: Health check (/ with retry) + run: | + for i in {1..30}; do + code=$(curl -s -o /dev/null -w "%{http_code}" "https://${{ secrets.EC2_HOST }}/" || true) + echo "Attempt $i: HTTP $code" + + # 200 OK / 301-302 Redirect면 "살아있다"로 간주 + if [ "$code" = "200" ] || [ "$code" = "301" ] || [ "$code" = "302" ]; then + echo "Health check OK" + exit 0 + fi + + sleep 4 + done + + echo "Health check FAILED" + exit 1 + diff --git a/build.gradle b/build.gradle index 8dbda75..ef3ac9b 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,7 @@ repositories { dependencies { // Web implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // JPA implementation 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/docker-compose.yml b/docker-compose.yml index 1fd0b0c..ca4109c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ services: app: build: . ports: - - "8080:8080" + - "127.0.0.1:8080:8080" depends_on: db: condition: service_healthy @@ -16,8 +16,6 @@ services: db: image: mysql:8.0 - ports: - - "3306:3306" environment: - MYSQL_DATABASE=${DB_NAME} - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} diff --git a/gradle.properties b/gradle.properties index adb8e0e..54d1aee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ -org.gradle.java.home=/usr/lib/jvm/java-17-amazon-corretto.x86_64 +# org.gradle.java.home=/usr/lib/jvm/java-17-amazon-corretto.x86_64 +# github action의 내장 jdk 사용 org.gradle.java.installations.paths=/usr/lib/jvm/java-17-amazon-corretto.x86_64 org.gradle.java.installations.auto-detect=false diff --git a/src/main/java/com/gdgoc/member/account/UserAuth.java b/src/main/java/com/gdgoc/member/account/UserAuth.java index c8ff68b..8d54fc3 100644 --- a/src/main/java/com/gdgoc/member/account/UserAuth.java +++ b/src/main/java/com/gdgoc/member/account/UserAuth.java @@ -50,7 +50,8 @@ public UserAuth(UUID userId, String externalUid, String email, String passwordHa public String getPasswordHash() { return passwordHash; } public Role getRole() { return role; } + public void setExternalUid(String externalUid) { this.externalUid = externalUid; } public void setEmail(String email) { this.email = email; } public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; } public void setRole(Role role) { this.role = role; } -} \ No newline at end of file +} diff --git a/src/main/java/com/gdgoc/member/account/UserAuthRepository.java b/src/main/java/com/gdgoc/member/account/UserAuthRepository.java index 1869de5..dfba80d 100644 --- a/src/main/java/com/gdgoc/member/account/UserAuthRepository.java +++ b/src/main/java/com/gdgoc/member/account/UserAuthRepository.java @@ -7,4 +7,5 @@ public interface UserAuthRepository extends JpaRepository { Optional findByExternalUid(String externalUid); -} \ No newline at end of file + Optional findByEmail(String email); +} diff --git a/src/main/java/com/gdgoc/member/security/CurrentUserService.java b/src/main/java/com/gdgoc/member/security/CurrentUserService.java index 0a63f5e..2afcea0 100644 --- a/src/main/java/com/gdgoc/member/security/CurrentUserService.java +++ b/src/main/java/com/gdgoc/member/security/CurrentUserService.java @@ -1,7 +1,7 @@ package com.gdgoc.member.security; -import com.gdgoc.member.account.AccountService; import com.gdgoc.member.account.UserAuth; +import com.gdgoc.member.account.UserAuthRepository; import com.gdgoc.member.global.error.UnauthorizedException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -11,10 +11,10 @@ @Service public class CurrentUserService { - private final AccountService accountService; + private final UserAuthRepository userAuthRepository; - public CurrentUserService(AccountService accountService) { - this.accountService = accountService; + public CurrentUserService(UserAuthRepository userAuthRepository) { + this.userAuthRepository = userAuthRepository; } public CurrentUser requireUser() { @@ -29,10 +29,9 @@ public CurrentUser requireUser() { } String subject = oidcUser.getSubject(); - String email = oidcUser.getEmail(); - String externalUid = "google:" + subject; - UserAuth userAuth = accountService.getOrCreate(externalUid, email); + UserAuth userAuth = userAuthRepository.findByExternalUid(subject) + .orElseThrow(() -> new IllegalStateException("User not found: " + subject)); return new CurrentUser( userAuth.getUserId(), @@ -42,4 +41,4 @@ public CurrentUser requireUser() { userAuth.getRole() ); } -} \ No newline at end of file +} diff --git a/src/main/java/com/gdgoc/member/security/OAuth2SuccessHandler.java b/src/main/java/com/gdgoc/member/security/OAuth2SuccessHandler.java index d7c23d3..dbeab76 100644 --- a/src/main/java/com/gdgoc/member/security/OAuth2SuccessHandler.java +++ b/src/main/java/com/gdgoc/member/security/OAuth2SuccessHandler.java @@ -1,7 +1,10 @@ package com.gdgoc.member.security; +import com.gdgoc.member.account.Role; import com.gdgoc.member.account.UserAuth; import com.gdgoc.member.account.UserAuthRepository; +import com.gdgoc.member.domain.profile.entity.Profile; +import com.gdgoc.member.domain.profile.repository.ProfileRepository; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -17,17 +20,61 @@ import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.Optional; @Component @RequiredArgsConstructor public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { private final UserAuthRepository userAuthRepository; + private final ProfileRepository profileRepository; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { OidcUser oidcUser = (OidcUser) authentication.getPrincipal(); - UserAuth userAuth = userAuthRepository.findByExternalUid(oidcUser.getName()).orElseThrow(); + + Optional userAuthOptional = userAuthRepository.findByExternalUid(oidcUser.getSubject()); + UserAuth userAuth; + + if (userAuthOptional.isPresent()) { + userAuth = userAuthOptional.get(); + } else { + Optional userAuthByEmailOptional = userAuthRepository.findByEmail(oidcUser.getEmail()); + if (userAuthByEmailOptional.isPresent()) { + userAuth = userAuthByEmailOptional.get(); + userAuth.setExternalUid(oidcUser.getSubject()); + userAuthRepository.save(userAuth); + } else { + UserAuth newUser = new UserAuth( + UUID.randomUUID(), + oidcUser.getSubject(), + oidcUser.getEmail(), + null, + Role.MEMBER + ); + userAuth = userAuthRepository.save(newUser); + } + } + + profileRepository.findByUserId(userAuth.getUserId()).or(() -> { + Profile newProfile = new Profile( + userAuth.getUserId(), + oidcUser.getEmail(), + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + profileRepository.save(newProfile); + return Optional.of(newProfile); + }); ClassPathResource resource = new ClassPathResource("static/oauth_success.html"); Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8); diff --git a/src/main/java/com/gdgoc/member/security/SecurityConfig.java b/src/main/java/com/gdgoc/member/security/SecurityConfig.java index 4b853d4..961aec4 100644 --- a/src/main/java/com/gdgoc/member/security/SecurityConfig.java +++ b/src/main/java/com/gdgoc/member/security/SecurityConfig.java @@ -4,6 +4,9 @@ import java.util.List; import com.fasterxml.jackson.databind.ObjectMapper; import com.gdgoc.member.BaseResponse; + +import lombok.RequiredArgsConstructor; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -19,12 +22,15 @@ @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { + private final OAuth2SuccessHandler oAuth2SuccessHandler; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .cors(Customizer.withDefaults()) // CORS + .cors(Customizer.withDefaults()) // CORS .csrf(csrf -> csrf.disable()) .exceptionHandling(e -> e .authenticationEntryPoint((request, response, authException) -> { @@ -66,7 +72,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ) .oauth2Login(oauth2 -> oauth2 .loginPage("/oauth2/authorization/google") - .defaultSuccessUrl("/api/v1/me/account", true) + .successHandler(oAuth2SuccessHandler) ) .formLogin(form -> form.disable()) .logout(logout -> logout @@ -81,11 +87,15 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOrigins(List.of("https://gdgoc-seoultech.vercel.app")); + config.setAllowedOrigins(List.of( + "http://localhost:5173", + "http://localhost:4173", + "https://gdgoc-seoultech.vercel.app" + )); config.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); - + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 3dc7ee5..ccedacd 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,77 +1,81 @@ spring: - application: - name: member + application: + name: member - mvc: - cors: - mappings: - "/api/**": - allowed-origins: "https://gdgoc-seoultech.vercel.app" - allowed-methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS" - allowed-headers: "*" - allow-credentials: true + mvc: + cors: + mappings: + "/api/**": + allowed-origins: "https://gdgoc-seoultech.vercel.app" + allowed-methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS" + allowed-headers: "*" + allow-credentials: true + config: + import: "optional:file:.env[.properties]" + # ========================= + # AWS s3 + # ========================= + cloud: + aws: + credentials: + access-key: ${AWS_ACCESS_KEY:} + secret-key: ${AWS_SECRET_KEY:} + region: + static: ${AWS_S3_REGION:ap-northeast-2} + s3: + bucket: ${AWS_S3_BUCKET:} - config: - import: "optional:file:.env[.properties]" - # ========================= - # AWS s3 - # ========================= - cloud: - aws: - credentials: - access-key: ${AWS_ACCESS_KEY:} - secret-key: ${AWS_SECRET_KEY:} - region: - static: ${AWS_S3_REGION:ap-northeast-2} - s3: - bucket: ${AWS_S3_BUCKET:} + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB - servlet: - multipart: - max-file-size: 10MB - max-request-size: 10MB + # ========================= + # Security (Google OAuth2) + # ========================= + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + scope: + - openid + - profile + - email - # ========================= - # Security (Google OAuth2) - # ========================= - security: - oauth2: - client: - registration: - google: - client-id: ${GOOGLE_CLIENT_ID} - client-secret: ${GOOGLE_CLIENT_SECRET} - scope: - - openid - - profile - - email - - # ========================= - # Datasource - # ========================= - datasource: - url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:gdg_post}?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8&allowPublicKeyRetrieval=true - username: ${DB_USERNAME:root} - password: ${DB_PASSWORD:} - driver-class-name: com.mysql.cj.jdbc.Driver - # ========================= - # JPA - # ========================= - jpa: - hibernate: - ddl-auto: update - show-sql: true - properties: - hibernate: - dialect: org.hibernate.dialect.MySQLDialect - format_sql: true + # ========================= + # Datasource + # ========================= + datasource: + url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:gdg_post}?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8&allowPublicKeyRetrieval=true + username: ${DB_USERNAME:root} + password: ${DB_PASSWORD:} + driver-class-name: com.mysql.cj.jdbc.Driver + # ========================= + # JPA + # ========================= + jpa: + hibernate: + ddl-auto: update + show-sql: true + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + format_sql: true server: - port: 8080 - address: 127.0.0.1 - forward-headers-strategy: framework + port: 8080 + address: 127.0.0.1 + forward-headers-strategy: framework + servlet: + session: + cookie: + same-site: none + secure: true logging: - level: - org.hibernate.SQL: debug + level: + org.hibernate.SQL: debug diff --git a/src/main/resources/static/oauth_success.html b/src/main/resources/static/oauth_success.html index 924cd81..acdb97d 100644 --- a/src/main/resources/static/oauth_success.html +++ b/src/main/resources/static/oauth_success.html @@ -9,7 +9,7 @@ const allowedOrigins = [ "http://localhost:5173", "http://localhost:4173", - "https://gdgoc-seoultech.vercel.app/", + "https://gdgoc-seoultech.vercel.app", ]; const message = {