From 2ca0d83b2de32dd17c9166dff4cb2f6f475783de Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Tue, 5 May 2026 00:47:35 +0900 Subject: [PATCH 01/13] deploy: prepare ec2 hosting --- .github/workflows/deploy.yml | 260 +++++++++--------- ontime-back/Dockerfile | 4 +- ontime-back/EC2_DEPLOY.md | 60 ++++ ontime-back/docker-compose.yml | 15 +- .../config/FirebaseInitialization.java | 32 ++- .../global/oauth/apple/AppleLoginService.java | 2 - 6 files changed, 229 insertions(+), 144 deletions(-) create mode 100644 ontime-back/EC2_DEPLOY.md diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3a822374..6bd862c9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,147 +1,159 @@ name: Deploy on: + workflow_dispatch: push: branches: - deploy jobs: - build-and-docker: + deploy-to-ec2: runs-on: ubuntu-latest + steps: - # 코드 체크아웃 - name: Checkout code - uses: actions/checkout@v3 - # JDK 설치 - - name: Set up JDK 17 # build.gradle 버전 확인 - uses: actions/setup-java@v2 + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - # Gradle Wrapper 검증 - - name: Set up Gradle - uses: gradle/wrapper-validation-action@v1 - # 설정 파일 생성 - - name: Create Config Files - run: | - mkdir -p ontime-back/src/main/resources - mkdir -p ontime-back/src/main/resources/key - echo "spring.application.name=${{ secrets.SPRING_APPLICATION_NAME }}" > ontime-back/src/main/resources/application.properties - echo "spring.datasource.url=${{ secrets.SPRING_DATASOURCE_URL }}" >> ontime-back/src/main/resources/application.properties - echo "spring.datasource.username=${{ secrets.SPRING_DATASOURCE_USERNAME }}" >> ontime-back/src/main/resources/application.properties - echo "spring.datasource.password=${{ secrets.SPRING_DATASOURCE_PASSWORD }}" >> ontime-back/src/main/resources/application.properties - echo "spring.datasource.driver-class-name=${{ secrets.SPRING_DATASOURCE_DRIVER_CLASS_NAME }}" >> ontime-back/src/main/resources/application.properties - echo "spring.jpa.hibernate.ddl-auto=${{ secrets.SPRING_JPA_HIBERNATE_DDL_AUTO }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.secret.key=${{ secrets.JWT_SECRETKEY }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.access.expiration=${{ secrets.JWT_ACCESS_EXPIRATION }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.refresh.expiration=${{ secrets.JWT_REFRESH_EXPIRATION }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.access.header=${{ secrets.JWT_ACCESS_HEADER }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.refresh.header=${{ secrets.JWT_REFRESH_HEADER }}" >> ontime-back/src/main/resources/application.properties - echo "google.web.client-id = ${{ secrets.GOOGLE_WEB_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties - echo "google.app.client-id = ${{ secrets.GOOGLE_APP_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.client-secret=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.scope=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_SCOPE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.redirect-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_REDIRECT_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.authorization-grant-type=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_AUTHORIZATION_GRANT_TYPE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.client-name=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_NAME }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.google.authorization-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_AUTHORIZATION_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.google.token-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_TOKEN_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.google.user-info-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_INFO_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.google.user-name-attribute=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_NAME_ATTRIBUTE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.client-id=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.scope=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_SCOPE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.redirect-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_REDIRECT_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.authorization-grant-type=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_AUTHORIZATION_GRANT_TYPE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.client-name=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_NAME }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.kakao.authorization-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_AUTHORIZATION_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.kakao.token-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_TOKEN_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.kakao.user-info-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_INFO_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.kakao.user-name-attribute=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_NAME_ATTRIBUTE }}" >> ontime-back/src/main/resources/application.properties - echo "apple.client.id=${{ secrets.APPLE_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties - echo "apple.client.secret=${{ secrets.APPLE_CLIENT_SECRET }}" >> ontime-back/src/main/resources/application.properties - echo "apple.login.key=${{ secrets.APPLE_LOGIN_KEY }}" >> ontime-back/src/main/resources/application.properties - echo "apple.team.id=${{ secrets.APPLE_TEAM_ID }}" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.enabled=true" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.url=${{ secrets.SPRING_FLYWAY_URL }}" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.user=${{ secrets.SPRING_FLYWAY_USER }}" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.password=${{ secrets.SPRING_FLYWAY_PASSWORD }}" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.baseline-on-migrate=true" >> ontime-back/src/main/resources/application.properties - echo "management.endpoints.web.exposure.include=health" >> ontime-back/src/main/resources/application.properties - echo "management.endpoint.health.show-details=always" >> ontime-back/src/main/resources/application.properties - echo "server.forward-headers-strategy=framework" >> ontime-back/src/main/resources/application.properties - echo "${{ secrets.ONTIME_PUSH_FIREBASE_ADMINSDK }}" > ontime-back/src/main/resources/ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json - - echo "${{ secrets.AUTHKEY_743M7R5W3W }}" > ontime-back/src/main/resources/key/AuthKey_743M7R5W3W.p8 - # Gradle 빌드 - name: Build with Gradle run: | - cd ontime-back + cd ontime-back ./gradlew build -x test - # 파일 위치 변경 - - name: Move file + + - name: Prepare deploy files + env: + SPRING_APPLICATION_NAME: ${{ secrets.SPRING_APPLICATION_NAME }} + SPRING_DATASOURCE_URL: ${{ secrets.SPRING_DATASOURCE_URL }} + SPRING_DATASOURCE_USERNAME: ${{ secrets.SPRING_DATASOURCE_USERNAME }} + SPRING_DATASOURCE_PASSWORD: ${{ secrets.SPRING_DATASOURCE_PASSWORD }} + SPRING_DATASOURCE_DRIVER_CLASS_NAME: ${{ secrets.SPRING_DATASOURCE_DRIVER_CLASS_NAME }} + SPRING_JPA_DATABASE_PLATFORM: ${{ secrets.SPRING_JPA_DATABASE_PLATFORM }} + SPRING_JPA_HIBERNATE_DDL_AUTO: ${{ secrets.SPRING_JPA_HIBERNATE_DDL_AUTO }} + JWT_SECRETKEY: ${{ secrets.JWT_SECRETKEY }} + JWT_ACCESS_EXPIRATION: ${{ secrets.JWT_ACCESS_EXPIRATION }} + JWT_REFRESH_EXPIRATION: ${{ secrets.JWT_REFRESH_EXPIRATION }} + JWT_ACCESS_HEADER: ${{ secrets.JWT_ACCESS_HEADER }} + JWT_REFRESH_HEADER: ${{ secrets.JWT_REFRESH_HEADER }} + GOOGLE_WEB_CLIENT_ID: ${{ secrets.GOOGLE_WEB_CLIENT_ID }} + GOOGLE_APP_CLIENT_ID: ${{ secrets.GOOGLE_APP_CLIENT_ID }} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET }} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_SCOPE: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_SCOPE }} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_REDIRECT_URI: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_REDIRECT_URI }} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_AUTHORIZATION_GRANT_TYPE: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_AUTHORIZATION_GRANT_TYPE }} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_NAME: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_NAME }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_AUTHORIZATION_URI: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_AUTHORIZATION_URI }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_TOKEN_URI: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_TOKEN_URI }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_INFO_URI: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_INFO_URI }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_NAME_ATTRIBUTE: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_NAME_ATTRIBUTE }} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID }} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_SCOPE: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_SCOPE }} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_REDIRECT_URI: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_REDIRECT_URI }} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_AUTHORIZATION_GRANT_TYPE: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_AUTHORIZATION_GRANT_TYPE }} + SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_NAME: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_NAME }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_AUTHORIZATION_URI: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_AUTHORIZATION_URI }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_TOKEN_URI: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_TOKEN_URI }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_INFO_URI: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_INFO_URI }} + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_NAME_ATTRIBUTE: ${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_NAME_ATTRIBUTE }} + APPLE_CLIENT_ID: ${{ secrets.APPLE_CLIENT_ID }} + APPLE_LOGIN_KEY: ${{ secrets.APPLE_LOGIN_KEY }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + FEATURE_APPLE_LOGIN_ENABLED: ${{ secrets.FEATURE_APPLE_LOGIN_ENABLED }} + AUTHKEY_743M7R5W3W: ${{ secrets.AUTHKEY_743M7R5W3W }} + SPRING_FLYWAY_URL: ${{ secrets.SPRING_FLYWAY_URL }} + SPRING_FLYWAY_USER: ${{ secrets.SPRING_FLYWAY_USER }} + SPRING_FLYWAY_PASSWORD: ${{ secrets.SPRING_FLYWAY_PASSWORD }} + ONTIME_PUSH_FIREBASE_ADMINSDK: ${{ secrets.ONTIME_PUSH_FIREBASE_ADMINSDK }} run: | - mv ontime-back/src/main/resources/ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json ./ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json - mv ontime-back/src/main/resources/application.properties ./application.properties - mv ontime-back/src/main/resources/key/AuthKey_743M7R5W3W.p8 ./AuthKey_743M7R5W3W.p8 - mv ontime-back/build/libs/ontime-back-0.0.1-SNAPSHOT.jar ./project.jar - mv ontime-back/docker-compose.yml ./docker-compose.yml - mv ontime-back/Dockerfile ./Dockerfile - # EC2 서버에 업로드 + mkdir -p config secrets + + add_property() { + printf '%s=%s\n' "$1" "$2" >> config/application.properties + } + + : > config/application.properties + add_property "spring.application.name" "$SPRING_APPLICATION_NAME" + add_property "spring.datasource.url" "$SPRING_DATASOURCE_URL" + add_property "spring.datasource.username" "$SPRING_DATASOURCE_USERNAME" + add_property "spring.datasource.password" "$SPRING_DATASOURCE_PASSWORD" + add_property "spring.datasource.driver-class-name" "$SPRING_DATASOURCE_DRIVER_CLASS_NAME" + add_property "spring.jpa.database" "mysql" + add_property "spring.jpa.database-platform" "${SPRING_JPA_DATABASE_PLATFORM:-org.hibernate.dialect.MySQL8Dialect}" + add_property "spring.jpa.hibernate.ddl-auto" "$SPRING_JPA_HIBERNATE_DDL_AUTO" + add_property "jwt.secret.key" "$JWT_SECRETKEY" + add_property "jwt.access.expiration" "$JWT_ACCESS_EXPIRATION" + add_property "jwt.refresh.expiration" "$JWT_REFRESH_EXPIRATION" + add_property "jwt.access.header" "$JWT_ACCESS_HEADER" + add_property "jwt.refresh.header" "$JWT_REFRESH_HEADER" + add_property "google.web.client-id" "$GOOGLE_WEB_CLIENT_ID" + add_property "google.app.client-id" "$GOOGLE_APP_CLIENT_ID" + add_property "spring.security.oauth2.client.registration.google.client-secret" "$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET" + add_property "spring.security.oauth2.client.registration.google.scope" "$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_SCOPE" + add_property "spring.security.oauth2.client.registration.google.redirect-uri" "$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_REDIRECT_URI" + add_property "spring.security.oauth2.client.registration.google.authorization-grant-type" "$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_AUTHORIZATION_GRANT_TYPE" + add_property "spring.security.oauth2.client.registration.google.client-name" "$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_NAME" + add_property "spring.security.oauth2.client.provider.google.authorization-uri" "$SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_AUTHORIZATION_URI" + add_property "spring.security.oauth2.client.provider.google.token-uri" "$SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_TOKEN_URI" + add_property "spring.security.oauth2.client.provider.google.user-info-uri" "$SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_INFO_URI" + add_property "spring.security.oauth2.client.provider.google.user-name-attribute" "$SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_NAME_ATTRIBUTE" + add_property "spring.security.oauth2.client.registration.kakao.client-id" "$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID" + add_property "spring.security.oauth2.client.registration.kakao.scope" "$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_SCOPE" + add_property "spring.security.oauth2.client.registration.kakao.redirect-uri" "$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_REDIRECT_URI" + add_property "spring.security.oauth2.client.registration.kakao.authorization-grant-type" "$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_AUTHORIZATION_GRANT_TYPE" + add_property "spring.security.oauth2.client.registration.kakao.client-name" "$SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_NAME" + add_property "spring.security.oauth2.client.provider.kakao.authorization-uri" "$SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_AUTHORIZATION_URI" + add_property "spring.security.oauth2.client.provider.kakao.token-uri" "$SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_TOKEN_URI" + add_property "spring.security.oauth2.client.provider.kakao.user-info-uri" "$SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_INFO_URI" + add_property "spring.security.oauth2.client.provider.kakao.user-name-attribute" "$SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_NAME_ATTRIBUTE" + add_property "apple.client.id" "$APPLE_CLIENT_ID" + add_property "apple.client.secret" "/app/secrets/AuthKey_743M7R5W3W.p8" + add_property "apple.login.key" "$APPLE_LOGIN_KEY" + add_property "apple.team.id" "$APPLE_TEAM_ID" + add_property "feature.apple-login.enabled" "${FEATURE_APPLE_LOGIN_ENABLED:-true}" + add_property "spring.flyway.enabled" "true" + add_property "spring.flyway.url" "$SPRING_FLYWAY_URL" + add_property "spring.flyway.user" "$SPRING_FLYWAY_USER" + add_property "spring.flyway.password" "$SPRING_FLYWAY_PASSWORD" + add_property "spring.flyway.baseline-on-migrate" "true" + add_property "management.endpoints.web.exposure.include" "health" + add_property "management.endpoint.health.show-details" "always" + add_property "server.forward-headers-strategy" "framework" + add_property "firebase.service-account.path" "/app/secrets/firebase-adminsdk.json" + + printf '%s' "$ONTIME_PUSH_FIREBASE_ADMINSDK" > secrets/firebase-adminsdk.json + printf '%s' "$AUTHKEY_743M7R5W3W" > secrets/AuthKey_743M7R5W3W.p8 + cp ontime-back/build/libs/ontime-back-0.0.1-SNAPSHOT.jar project.jar + cp ontime-back/Dockerfile Dockerfile + cp ontime-back/docker-compose.yml docker-compose.yml + - name: Upload files to EC2 uses: appleboy/scp-action@v0.1.7 with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_SSH_KEY }} - source: "./project.jar, ./docker-compose.yml, ./Dockerfile, ./ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json, ./application.properties, ./AuthKey_743M7R5W3W.p8" - target: "/home/ubuntu/OnTime-back" - debug: true - create-config-files: - needs: build-and-docker - runs-on: ubuntu-latest - steps: - # EC2 서버에 접근 + 설정 파일 생성 - - name: SSH to EC2 & Create Config Files - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_SSH_KEY }} - debug: true - script: | - sudo mkdir -p /home/ubuntu/OnTime-back/ontime-back/src/main/resources/key - sudo mv /home/ubuntu/OnTime-back/ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json /home/ubuntu/OnTime-back/ontime-back/src/main/resources/ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json - sudo mv /home/ubuntu/OnTime-back/application.properties /home/ubuntu/OnTime-back/ontime-back/src/main/resources/application.properties - sudo mv /home/ubuntu/OnTime-back/AuthKey_743M7R5W3W.p8 /home/ubuntu/OnTime-back/ontime-back/src/main/resources/key/AuthKey_743M7R5W3W.p8 + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + source: "project.jar,Dockerfile,docker-compose.yml,config/application.properties,secrets/firebase-adminsdk.json,secrets/AuthKey_743M7R5W3W.p8" + target: "/home/ubuntu/OnTime-back" - deploy-to-ec2: - needs: create-config-files - runs-on: ubuntu-latest - steps: - # EC2 서버에 접근 + docker container 배포 - - name: SSH to EC2 & Deploy Docker Containers - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_SSH_KEY }} - script: | - # 기존 컨테이너 종료 및 삭제 - CONTAINER_ID=$(sudo docker ps -aq --filter "name=ontime-container") - if [ ! -z "$CONTAINER_ID" ]; then - sudo docker stop $CONTAINER_ID - sudo docker rm $CONTAINER_ID - fi - - # 기존 컨테이너 및 볼륨 정리 - sudo docker-compose down - sudo docker container prune -f - sudo docker image prune -a -f - sudo docker volume prune -f - sudo docker network prune -f - - # Docker Compose 실행 - cd /home/ubuntu/OnTime-back - docker-compose up --build -d - + - name: Restart service on EC2 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + set -e + cd /home/ubuntu/OnTime-back + sudo docker rm -f ontime-container || true + if sudo docker compose version >/dev/null 2>&1; then + sudo docker compose down + sudo docker compose up --build -d + else + sudo docker-compose down + sudo docker-compose up --build -d + fi + sudo docker image prune -f diff --git a/ontime-back/Dockerfile b/ontime-back/Dockerfile index fdc20045..213213e0 100644 --- a/ontime-back/Dockerfile +++ b/ontime-back/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:17-jdk +FROM eclipse-temurin:17-jre RUN apt-get update && \ apt-get install -y tzdata && \ ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \ @@ -7,4 +7,4 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* WORKDIR /app COPY project.jar app.jar -ENTRYPOINT ["java", "-jar", "/app/app.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-jar", "/app/app.jar"] diff --git a/ontime-back/EC2_DEPLOY.md b/ontime-back/EC2_DEPLOY.md new file mode 100644 index 00000000..c1a07594 --- /dev/null +++ b/ontime-back/EC2_DEPLOY.md @@ -0,0 +1,60 @@ +# EC2 Deployment + +This service deploys to Amazon EC2 through `.github/workflows/deploy.yml`. + +## How to Deploy + +1. Make sure the EC2 instance has Docker installed and the security group allows inbound traffic for the service port, currently `8080`. +2. Add the required GitHub Actions secrets listed below. +3. Run the `Deploy` workflow manually from GitHub Actions, or push to the `deploy` branch. + +The workflow builds the Spring Boot jar, creates deploy-only config files from GitHub Secrets, uploads them to `/home/ubuntu/OnTime-back`, and restarts Docker Compose on the EC2 instance. + +## Required EC2 Secrets + +- `EC2_HOST` +- `EC2_USER` +- `EC2_SSH_KEY` + +## Required Application Secrets + +- `SPRING_APPLICATION_NAME` +- `SPRING_DATASOURCE_URL` +- `SPRING_DATASOURCE_USERNAME` +- `SPRING_DATASOURCE_PASSWORD` +- `SPRING_DATASOURCE_DRIVER_CLASS_NAME` +- `SPRING_JPA_HIBERNATE_DDL_AUTO` +- `JWT_SECRETKEY` +- `JWT_ACCESS_EXPIRATION` +- `JWT_REFRESH_EXPIRATION` +- `JWT_ACCESS_HEADER` +- `JWT_REFRESH_HEADER` +- `GOOGLE_WEB_CLIENT_ID` +- `GOOGLE_APP_CLIENT_ID` +- `APPLE_CLIENT_ID` +- `APPLE_LOGIN_KEY` +- `APPLE_TEAM_ID` +- `AUTHKEY_743M7R5W3W` +- `SPRING_FLYWAY_URL` +- `SPRING_FLYWAY_USER` +- `SPRING_FLYWAY_PASSWORD` +- `ONTIME_PUSH_FIREBASE_ADMINSDK` + +## Optional Secrets + +- `SPRING_JPA_DATABASE_PLATFORM` defaults to `org.hibernate.dialect.MySQL8Dialect`. +- `FEATURE_APPLE_LOGIN_ENABLED` defaults to `true`. +- Google and Kakao OAuth provider/registration secrets are included by the workflow when configured. + +## Runtime Files on EC2 + +The deploy workflow writes these files under `/home/ubuntu/OnTime-back`: + +- `project.jar` +- `Dockerfile` +- `docker-compose.yml` +- `config/application.properties` +- `secrets/firebase-adminsdk.json` +- `secrets/AuthKey_743M7R5W3W.p8` + +Do not commit local `application.properties`, Firebase service account JSON, Apple `.p8` keys, or `.env` files. diff --git a/ontime-back/docker-compose.yml b/ontime-back/docker-compose.yml index 88d6cc61..81cd810d 100644 --- a/ontime-back/docker-compose.yml +++ b/ontime-back/docker-compose.yml @@ -1,15 +1,14 @@ -version: "3.8" - services: backend: build: context: . - dockerfile: Dockerfile # Dockerfile 이름 - image: ontimedemo # 빌드된 백엔드 이미지 - container_name: ontime-container + dockerfile: Dockerfile + image: ontime-backend + container_name: ontime-backend + restart: unless-stopped ports: - "8080:8080" - - "8443:8443" volumes: - - /home/ubuntu/OnTime-back/ontime-back/src/main/resources/:/app/src/main/resources/ - - /home/ubuntu/OnTime-back/ontime-back/src/main/resources/key/:/app/resources/key/ + - ./config/application.properties:/app/config/application.properties:ro + - ./secrets/firebase-adminsdk.json:/app/secrets/firebase-adminsdk.json:ro + - ./secrets/AuthKey_743M7R5W3W.p8:/app/secrets/AuthKey_743M7R5W3W.p8:ro diff --git a/ontime-back/src/main/java/devkor/ontime_back/config/FirebaseInitialization.java b/ontime-back/src/main/java/devkor/ontime_back/config/FirebaseInitialization.java index 3e543af7..802468a5 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/config/FirebaseInitialization.java +++ b/ontime-back/src/main/java/devkor/ontime_back/config/FirebaseInitialization.java @@ -4,6 +4,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; @@ -13,23 +14,38 @@ import java.io.InputStream; @Service +@Slf4j public class FirebaseInitialization { + private static final String DEFAULT_FIREBASE_RESOURCE = "ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json"; + + @Value("${firebase.service-account.path:}") + private String serviceAccountPath; + @PostConstruct public void initialize() { - try { - InputStream serviceAccount = getClass().getClassLoader().getResourceAsStream("ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json"); - if (serviceAccount == null) { - throw new FileNotFoundException("Resource not found: ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json"); - } - + try (InputStream serviceAccount = openServiceAccount()) { FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .build(); - FirebaseApp.initializeApp(options); + if (FirebaseApp.getApps().isEmpty()) { + FirebaseApp.initializeApp(options); + } } catch (IOException e) { - e.printStackTrace(); + log.error("Failed to initialize Firebase", e); + } + } + + private InputStream openServiceAccount() throws IOException { + if (serviceAccountPath != null && !serviceAccountPath.isBlank()) { + return new FileInputStream(serviceAccountPath); + } + + InputStream serviceAccount = getClass().getClassLoader().getResourceAsStream(DEFAULT_FIREBASE_RESOURCE); + if (serviceAccount == null) { + throw new FileNotFoundException("Resource not found: " + DEFAULT_FIREBASE_RESOURCE); } + return serviceAccount; } } diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginService.java b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginService.java index d58d67cc..69aa948a 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginService.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginService.java @@ -187,7 +187,6 @@ public AppleTokenResponseDto getAppleAccessTokenAndRefreshToken(String authCode) String clientSecret = generateClientSecret(); log.info("getAppleAccessTokenAndRefreshToken"); log.info("client_id: {}", clientId); - log.info("client_secret: {}", clientSecret); MultiValueMap requestBody = new LinkedMultiValueMap<>(); requestBody.add("grant_type", "authorization_code"); requestBody.add("code", authCode); @@ -270,4 +269,3 @@ public boolean revokeToken(Long userId) throws Exception { } } } - From 0b601c0408313896b99e6dab397ef60a8fc816c7 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Tue, 5 May 2026 01:31:14 +0900 Subject: [PATCH 02/13] deploy: run mysql with backend compose --- .github/workflows/deploy.yml | 27 ++++++++++++++++++--------- ontime-back/EC2_DEPLOY.md | 11 ++++++----- ontime-back/docker-compose.yml | 23 +++++++++++++++++++++++ 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6bd862c9..3550cdd1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,10 +28,11 @@ jobs: - name: Prepare deploy files env: SPRING_APPLICATION_NAME: ${{ secrets.SPRING_APPLICATION_NAME }} - SPRING_DATASOURCE_URL: ${{ secrets.SPRING_DATASOURCE_URL }} SPRING_DATASOURCE_USERNAME: ${{ secrets.SPRING_DATASOURCE_USERNAME }} SPRING_DATASOURCE_PASSWORD: ${{ secrets.SPRING_DATASOURCE_PASSWORD }} SPRING_DATASOURCE_DRIVER_CLASS_NAME: ${{ secrets.SPRING_DATASOURCE_DRIVER_CLASS_NAME }} + MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} + MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} SPRING_JPA_DATABASE_PLATFORM: ${{ secrets.SPRING_JPA_DATABASE_PLATFORM }} SPRING_JPA_HIBERNATE_DDL_AUTO: ${{ secrets.SPRING_JPA_HIBERNATE_DDL_AUTO }} JWT_SECRETKEY: ${{ secrets.JWT_SECRETKEY }} @@ -64,20 +65,21 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} FEATURE_APPLE_LOGIN_ENABLED: ${{ secrets.FEATURE_APPLE_LOGIN_ENABLED }} AUTHKEY_743M7R5W3W: ${{ secrets.AUTHKEY_743M7R5W3W }} - SPRING_FLYWAY_URL: ${{ secrets.SPRING_FLYWAY_URL }} - SPRING_FLYWAY_USER: ${{ secrets.SPRING_FLYWAY_USER }} - SPRING_FLYWAY_PASSWORD: ${{ secrets.SPRING_FLYWAY_PASSWORD }} ONTIME_PUSH_FIREBASE_ADMINSDK: ${{ secrets.ONTIME_PUSH_FIREBASE_ADMINSDK }} run: | mkdir -p config secrets + mysql_database="${MYSQL_DATABASE:-ontime}" + mysql_root_password="${MYSQL_ROOT_PASSWORD:-$SPRING_DATASOURCE_PASSWORD}" + mysql_url="jdbc:mysql://mysql:3306/${mysql_database}?serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true" + add_property() { printf '%s=%s\n' "$1" "$2" >> config/application.properties } : > config/application.properties add_property "spring.application.name" "$SPRING_APPLICATION_NAME" - add_property "spring.datasource.url" "$SPRING_DATASOURCE_URL" + add_property "spring.datasource.url" "$mysql_url" add_property "spring.datasource.username" "$SPRING_DATASOURCE_USERNAME" add_property "spring.datasource.password" "$SPRING_DATASOURCE_PASSWORD" add_property "spring.datasource.driver-class-name" "$SPRING_DATASOURCE_DRIVER_CLASS_NAME" @@ -115,15 +117,22 @@ jobs: add_property "apple.team.id" "$APPLE_TEAM_ID" add_property "feature.apple-login.enabled" "${FEATURE_APPLE_LOGIN_ENABLED:-true}" add_property "spring.flyway.enabled" "true" - add_property "spring.flyway.url" "$SPRING_FLYWAY_URL" - add_property "spring.flyway.user" "$SPRING_FLYWAY_USER" - add_property "spring.flyway.password" "$SPRING_FLYWAY_PASSWORD" + add_property "spring.flyway.url" "$mysql_url" + add_property "spring.flyway.user" "$SPRING_DATASOURCE_USERNAME" + add_property "spring.flyway.password" "$SPRING_DATASOURCE_PASSWORD" add_property "spring.flyway.baseline-on-migrate" "true" add_property "management.endpoints.web.exposure.include" "health" add_property "management.endpoint.health.show-details" "always" add_property "server.forward-headers-strategy" "framework" add_property "firebase.service-account.path" "/app/secrets/firebase-adminsdk.json" + { + printf 'MYSQL_DATABASE=%s\n' "$mysql_database" + printf 'MYSQL_USER=%s\n' "$SPRING_DATASOURCE_USERNAME" + printf 'MYSQL_PASSWORD=%s\n' "$SPRING_DATASOURCE_PASSWORD" + printf 'MYSQL_ROOT_PASSWORD=%s\n' "$mysql_root_password" + } > config/mysql.env + printf '%s' "$ONTIME_PUSH_FIREBASE_ADMINSDK" > secrets/firebase-adminsdk.json printf '%s' "$AUTHKEY_743M7R5W3W" > secrets/AuthKey_743M7R5W3W.p8 cp ontime-back/build/libs/ontime-back-0.0.1-SNAPSHOT.jar project.jar @@ -136,7 +145,7 @@ jobs: host: ${{ secrets.EC2_HOST }} username: ${{ secrets.EC2_USER }} key: ${{ secrets.EC2_SSH_KEY }} - source: "project.jar,Dockerfile,docker-compose.yml,config/application.properties,secrets/firebase-adminsdk.json,secrets/AuthKey_743M7R5W3W.p8" + source: "project.jar,Dockerfile,docker-compose.yml,config/application.properties,config/mysql.env,secrets/firebase-adminsdk.json,secrets/AuthKey_743M7R5W3W.p8" target: "/home/ubuntu/OnTime-back" - name: Restart service on EC2 diff --git a/ontime-back/EC2_DEPLOY.md b/ontime-back/EC2_DEPLOY.md index c1a07594..02ae9e9e 100644 --- a/ontime-back/EC2_DEPLOY.md +++ b/ontime-back/EC2_DEPLOY.md @@ -8,7 +8,7 @@ This service deploys to Amazon EC2 through `.github/workflows/deploy.yml`. 2. Add the required GitHub Actions secrets listed below. 3. Run the `Deploy` workflow manually from GitHub Actions, or push to the `deploy` branch. -The workflow builds the Spring Boot jar, creates deploy-only config files from GitHub Secrets, uploads them to `/home/ubuntu/OnTime-back`, and restarts Docker Compose on the EC2 instance. +The workflow builds the Spring Boot jar, creates deploy-only config files from GitHub Secrets, uploads them to `/home/ubuntu/OnTime-back`, and restarts Docker Compose on the EC2 instance. Docker Compose runs both the backend and a private MySQL 8 container on the same Docker network. ## Required EC2 Secrets @@ -19,11 +19,11 @@ The workflow builds the Spring Boot jar, creates deploy-only config files from G ## Required Application Secrets - `SPRING_APPLICATION_NAME` -- `SPRING_DATASOURCE_URL` - `SPRING_DATASOURCE_USERNAME` - `SPRING_DATASOURCE_PASSWORD` - `SPRING_DATASOURCE_DRIVER_CLASS_NAME` - `SPRING_JPA_HIBERNATE_DDL_AUTO` +- `MYSQL_ROOT_PASSWORD` - `JWT_SECRETKEY` - `JWT_ACCESS_EXPIRATION` - `JWT_REFRESH_EXPIRATION` @@ -35,14 +35,12 @@ The workflow builds the Spring Boot jar, creates deploy-only config files from G - `APPLE_LOGIN_KEY` - `APPLE_TEAM_ID` - `AUTHKEY_743M7R5W3W` -- `SPRING_FLYWAY_URL` -- `SPRING_FLYWAY_USER` -- `SPRING_FLYWAY_PASSWORD` - `ONTIME_PUSH_FIREBASE_ADMINSDK` ## Optional Secrets - `SPRING_JPA_DATABASE_PLATFORM` defaults to `org.hibernate.dialect.MySQL8Dialect`. +- `MYSQL_DATABASE` defaults to `ontime`. - `FEATURE_APPLE_LOGIN_ENABLED` defaults to `true`. - Google and Kakao OAuth provider/registration secrets are included by the workflow when configured. @@ -54,7 +52,10 @@ The deploy workflow writes these files under `/home/ubuntu/OnTime-back`: - `Dockerfile` - `docker-compose.yml` - `config/application.properties` +- `config/mysql.env` - `secrets/firebase-adminsdk.json` - `secrets/AuthKey_743M7R5W3W.p8` +MySQL data is stored in the Docker volume `mysql-data`. Removing that volume deletes the deployed database. + Do not commit local `application.properties`, Firebase service account JSON, Apple `.p8` keys, or `.env` files. diff --git a/ontime-back/docker-compose.yml b/ontime-back/docker-compose.yml index 81cd810d..36a58d22 100644 --- a/ontime-back/docker-compose.yml +++ b/ontime-back/docker-compose.yml @@ -1,4 +1,21 @@ services: + mysql: + image: mysql:8.0 + container_name: ontime-mysql + restart: unless-stopped + env_file: + - ./config/mysql.env + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + volumes: + - mysql-data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"] + interval: 10s + timeout: 5s + retries: 10 + backend: build: context: . @@ -6,9 +23,15 @@ services: image: ontime-backend container_name: ontime-backend restart: unless-stopped + depends_on: + mysql: + condition: service_healthy ports: - "8080:8080" volumes: - ./config/application.properties:/app/config/application.properties:ro - ./secrets/firebase-adminsdk.json:/app/secrets/firebase-adminsdk.json:ro - ./secrets/AuthKey_743M7R5W3W.p8:/app/secrets/AuthKey_743M7R5W3W.p8:ro + +volumes: + mysql-data: From cfc0fdf22f33a360ca88f8ae626e18b659e8085b Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Tue, 5 May 2026 01:32:59 +0900 Subject: [PATCH 03/13] docs: move ec2 deployment guide --- ontime-back/{EC2_DEPLOY.md => docs/deployment/ec2.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ontime-back/{EC2_DEPLOY.md => docs/deployment/ec2.md} (100%) diff --git a/ontime-back/EC2_DEPLOY.md b/ontime-back/docs/deployment/ec2.md similarity index 100% rename from ontime-back/EC2_DEPLOY.md rename to ontime-back/docs/deployment/ec2.md From 9833d60defc512578b91448718028472dc6650fd Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Tue, 5 May 2026 01:37:05 +0900 Subject: [PATCH 04/13] deploy: preserve mysql volume explicitly --- ontime-back/docker-compose.yml | 1 + ontime-back/docs/deployment/ec2.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ontime-back/docker-compose.yml b/ontime-back/docker-compose.yml index 36a58d22..730b7eac 100644 --- a/ontime-back/docker-compose.yml +++ b/ontime-back/docker-compose.yml @@ -35,3 +35,4 @@ services: volumes: mysql-data: + name: ontime-mysql-data diff --git a/ontime-back/docs/deployment/ec2.md b/ontime-back/docs/deployment/ec2.md index 02ae9e9e..e7e6c01b 100644 --- a/ontime-back/docs/deployment/ec2.md +++ b/ontime-back/docs/deployment/ec2.md @@ -56,6 +56,8 @@ The deploy workflow writes these files under `/home/ubuntu/OnTime-back`: - `secrets/firebase-adminsdk.json` - `secrets/AuthKey_743M7R5W3W.p8` -MySQL data is stored in the Docker volume `mysql-data`. Removing that volume deletes the deployed database. +MySQL data is stored in the Docker volume `ontime-mysql-data`. Normal deploys run `docker compose down` without `-v`, so this volume is preserved across backend redeploys. Removing that volume deletes the deployed database. + +Keep `SPRING_JPA_HIBERNATE_DDL_AUTO` set to `validate` or another non-destructive value for production. Values such as `create` or `create-drop` can recreate schema and destroy data. Do not commit local `application.properties`, Firebase service account JSON, Apple `.p8` keys, or `.env` files. From 4a50fb328a076cfb88b49e22a402783926a1e301 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Tue, 5 May 2026 01:46:39 +0900 Subject: [PATCH 05/13] deploy: rollback compose mysql --- .github/workflows/deploy.yml | 27 +++++++++------------------ ontime-back/docker-compose.yml | 24 ------------------------ ontime-back/docs/deployment/ec2.md | 13 +++++-------- 3 files changed, 14 insertions(+), 50 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3550cdd1..6bd862c9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,11 +28,10 @@ jobs: - name: Prepare deploy files env: SPRING_APPLICATION_NAME: ${{ secrets.SPRING_APPLICATION_NAME }} + SPRING_DATASOURCE_URL: ${{ secrets.SPRING_DATASOURCE_URL }} SPRING_DATASOURCE_USERNAME: ${{ secrets.SPRING_DATASOURCE_USERNAME }} SPRING_DATASOURCE_PASSWORD: ${{ secrets.SPRING_DATASOURCE_PASSWORD }} SPRING_DATASOURCE_DRIVER_CLASS_NAME: ${{ secrets.SPRING_DATASOURCE_DRIVER_CLASS_NAME }} - MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} - MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} SPRING_JPA_DATABASE_PLATFORM: ${{ secrets.SPRING_JPA_DATABASE_PLATFORM }} SPRING_JPA_HIBERNATE_DDL_AUTO: ${{ secrets.SPRING_JPA_HIBERNATE_DDL_AUTO }} JWT_SECRETKEY: ${{ secrets.JWT_SECRETKEY }} @@ -65,21 +64,20 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} FEATURE_APPLE_LOGIN_ENABLED: ${{ secrets.FEATURE_APPLE_LOGIN_ENABLED }} AUTHKEY_743M7R5W3W: ${{ secrets.AUTHKEY_743M7R5W3W }} + SPRING_FLYWAY_URL: ${{ secrets.SPRING_FLYWAY_URL }} + SPRING_FLYWAY_USER: ${{ secrets.SPRING_FLYWAY_USER }} + SPRING_FLYWAY_PASSWORD: ${{ secrets.SPRING_FLYWAY_PASSWORD }} ONTIME_PUSH_FIREBASE_ADMINSDK: ${{ secrets.ONTIME_PUSH_FIREBASE_ADMINSDK }} run: | mkdir -p config secrets - mysql_database="${MYSQL_DATABASE:-ontime}" - mysql_root_password="${MYSQL_ROOT_PASSWORD:-$SPRING_DATASOURCE_PASSWORD}" - mysql_url="jdbc:mysql://mysql:3306/${mysql_database}?serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true" - add_property() { printf '%s=%s\n' "$1" "$2" >> config/application.properties } : > config/application.properties add_property "spring.application.name" "$SPRING_APPLICATION_NAME" - add_property "spring.datasource.url" "$mysql_url" + add_property "spring.datasource.url" "$SPRING_DATASOURCE_URL" add_property "spring.datasource.username" "$SPRING_DATASOURCE_USERNAME" add_property "spring.datasource.password" "$SPRING_DATASOURCE_PASSWORD" add_property "spring.datasource.driver-class-name" "$SPRING_DATASOURCE_DRIVER_CLASS_NAME" @@ -117,22 +115,15 @@ jobs: add_property "apple.team.id" "$APPLE_TEAM_ID" add_property "feature.apple-login.enabled" "${FEATURE_APPLE_LOGIN_ENABLED:-true}" add_property "spring.flyway.enabled" "true" - add_property "spring.flyway.url" "$mysql_url" - add_property "spring.flyway.user" "$SPRING_DATASOURCE_USERNAME" - add_property "spring.flyway.password" "$SPRING_DATASOURCE_PASSWORD" + add_property "spring.flyway.url" "$SPRING_FLYWAY_URL" + add_property "spring.flyway.user" "$SPRING_FLYWAY_USER" + add_property "spring.flyway.password" "$SPRING_FLYWAY_PASSWORD" add_property "spring.flyway.baseline-on-migrate" "true" add_property "management.endpoints.web.exposure.include" "health" add_property "management.endpoint.health.show-details" "always" add_property "server.forward-headers-strategy" "framework" add_property "firebase.service-account.path" "/app/secrets/firebase-adminsdk.json" - { - printf 'MYSQL_DATABASE=%s\n' "$mysql_database" - printf 'MYSQL_USER=%s\n' "$SPRING_DATASOURCE_USERNAME" - printf 'MYSQL_PASSWORD=%s\n' "$SPRING_DATASOURCE_PASSWORD" - printf 'MYSQL_ROOT_PASSWORD=%s\n' "$mysql_root_password" - } > config/mysql.env - printf '%s' "$ONTIME_PUSH_FIREBASE_ADMINSDK" > secrets/firebase-adminsdk.json printf '%s' "$AUTHKEY_743M7R5W3W" > secrets/AuthKey_743M7R5W3W.p8 cp ontime-back/build/libs/ontime-back-0.0.1-SNAPSHOT.jar project.jar @@ -145,7 +136,7 @@ jobs: host: ${{ secrets.EC2_HOST }} username: ${{ secrets.EC2_USER }} key: ${{ secrets.EC2_SSH_KEY }} - source: "project.jar,Dockerfile,docker-compose.yml,config/application.properties,config/mysql.env,secrets/firebase-adminsdk.json,secrets/AuthKey_743M7R5W3W.p8" + source: "project.jar,Dockerfile,docker-compose.yml,config/application.properties,secrets/firebase-adminsdk.json,secrets/AuthKey_743M7R5W3W.p8" target: "/home/ubuntu/OnTime-back" - name: Restart service on EC2 diff --git a/ontime-back/docker-compose.yml b/ontime-back/docker-compose.yml index 730b7eac..81cd810d 100644 --- a/ontime-back/docker-compose.yml +++ b/ontime-back/docker-compose.yml @@ -1,21 +1,4 @@ services: - mysql: - image: mysql:8.0 - container_name: ontime-mysql - restart: unless-stopped - env_file: - - ./config/mysql.env - command: - - --character-set-server=utf8mb4 - - --collation-server=utf8mb4_unicode_ci - volumes: - - mysql-data:/var/lib/mysql - healthcheck: - test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"] - interval: 10s - timeout: 5s - retries: 10 - backend: build: context: . @@ -23,16 +6,9 @@ services: image: ontime-backend container_name: ontime-backend restart: unless-stopped - depends_on: - mysql: - condition: service_healthy ports: - "8080:8080" volumes: - ./config/application.properties:/app/config/application.properties:ro - ./secrets/firebase-adminsdk.json:/app/secrets/firebase-adminsdk.json:ro - ./secrets/AuthKey_743M7R5W3W.p8:/app/secrets/AuthKey_743M7R5W3W.p8:ro - -volumes: - mysql-data: - name: ontime-mysql-data diff --git a/ontime-back/docs/deployment/ec2.md b/ontime-back/docs/deployment/ec2.md index e7e6c01b..c1a07594 100644 --- a/ontime-back/docs/deployment/ec2.md +++ b/ontime-back/docs/deployment/ec2.md @@ -8,7 +8,7 @@ This service deploys to Amazon EC2 through `.github/workflows/deploy.yml`. 2. Add the required GitHub Actions secrets listed below. 3. Run the `Deploy` workflow manually from GitHub Actions, or push to the `deploy` branch. -The workflow builds the Spring Boot jar, creates deploy-only config files from GitHub Secrets, uploads them to `/home/ubuntu/OnTime-back`, and restarts Docker Compose on the EC2 instance. Docker Compose runs both the backend and a private MySQL 8 container on the same Docker network. +The workflow builds the Spring Boot jar, creates deploy-only config files from GitHub Secrets, uploads them to `/home/ubuntu/OnTime-back`, and restarts Docker Compose on the EC2 instance. ## Required EC2 Secrets @@ -19,11 +19,11 @@ The workflow builds the Spring Boot jar, creates deploy-only config files from G ## Required Application Secrets - `SPRING_APPLICATION_NAME` +- `SPRING_DATASOURCE_URL` - `SPRING_DATASOURCE_USERNAME` - `SPRING_DATASOURCE_PASSWORD` - `SPRING_DATASOURCE_DRIVER_CLASS_NAME` - `SPRING_JPA_HIBERNATE_DDL_AUTO` -- `MYSQL_ROOT_PASSWORD` - `JWT_SECRETKEY` - `JWT_ACCESS_EXPIRATION` - `JWT_REFRESH_EXPIRATION` @@ -35,12 +35,14 @@ The workflow builds the Spring Boot jar, creates deploy-only config files from G - `APPLE_LOGIN_KEY` - `APPLE_TEAM_ID` - `AUTHKEY_743M7R5W3W` +- `SPRING_FLYWAY_URL` +- `SPRING_FLYWAY_USER` +- `SPRING_FLYWAY_PASSWORD` - `ONTIME_PUSH_FIREBASE_ADMINSDK` ## Optional Secrets - `SPRING_JPA_DATABASE_PLATFORM` defaults to `org.hibernate.dialect.MySQL8Dialect`. -- `MYSQL_DATABASE` defaults to `ontime`. - `FEATURE_APPLE_LOGIN_ENABLED` defaults to `true`. - Google and Kakao OAuth provider/registration secrets are included by the workflow when configured. @@ -52,12 +54,7 @@ The deploy workflow writes these files under `/home/ubuntu/OnTime-back`: - `Dockerfile` - `docker-compose.yml` - `config/application.properties` -- `config/mysql.env` - `secrets/firebase-adminsdk.json` - `secrets/AuthKey_743M7R5W3W.p8` -MySQL data is stored in the Docker volume `ontime-mysql-data`. Normal deploys run `docker compose down` without `-v`, so this volume is preserved across backend redeploys. Removing that volume deletes the deployed database. - -Keep `SPRING_JPA_HIBERNATE_DDL_AUTO` set to `validate` or another non-destructive value for production. Values such as `create` or `create-drop` can recreate schema and destroy data. - Do not commit local `application.properties`, Firebase service account JSON, Apple `.p8` keys, or `.env` files. From ca672c36bd873493e415fddfb1c90ccc81f93b94 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Wed, 6 May 2026 02:27:29 +0900 Subject: [PATCH 06/13] Handle duplicate Google social users --- .../global/oauth/google/GoogleLoginFilter.java | 14 +++++++++----- .../ontime_back/repository/UserRepository.java | 5 ++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java index 928a90b7..34833cb7 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java @@ -18,8 +18,8 @@ import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import java.io.IOException; +import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @Slf4j @@ -48,10 +48,14 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ Object loginLock = LOGIN_LOCKS.computeIfAbsent(googleUserId, key -> new Object()); synchronized (loginLock) { - Optional existingUser = userRepository.findBySocialTypeAndSocialId(SocialType.GOOGLE, googleUserId); - - if (existingUser.isPresent()) { - return googleLoginService.handleLogin(oAuthGoogleRequestDto, existingUser.get(), response); + List existingUsers = userRepository.findAllBySocialTypeAndSocialIdOrderByIdDesc(SocialType.GOOGLE, googleUserId); + + if (!existingUsers.isEmpty()) { + if (existingUsers.size() > 1) { + log.warn("동일한 Google socialId를 가진 유저가 {}명 존재합니다. 최신 userId={} 계정으로 로그인합니다.", + existingUsers.size(), existingUsers.get(0).getId()); + } + return googleLoginService.handleLogin(oAuthGoogleRequestDto, existingUsers.get(0), response); } else { OAuthGoogleUserDto oAuthGoogleUserDto = new OAuthGoogleUserDto(googleUserId, (String) googlePayload.get("name"), (String) googlePayload.get("picture"), googlePayload.getEmail()); return googleLoginService.handleRegister(oAuthGoogleRequestDto, oAuthGoogleUserDto, response); diff --git a/ontime-back/src/main/java/devkor/ontime_back/repository/UserRepository.java b/ontime-back/src/main/java/devkor/ontime_back/repository/UserRepository.java index d00c55d7..dc745b79 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/repository/UserRepository.java +++ b/ontime-back/src/main/java/devkor/ontime_back/repository/UserRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository @@ -21,8 +22,10 @@ public interface UserRepository extends JpaRepository { // 추가정보 입력받을때 사용 Optional findBySocialTypeAndSocialId(SocialType socialType, String socialId); + List findAllBySocialTypeAndSocialIdOrderByIdDesc(SocialType socialType, String socialId); + @Query("SELECT u.spareTime FROM User u WHERE u.id = :id") Integer findSpareTimeById(Long id); Optional findByAccessToken(String token); -} \ No newline at end of file +} From b94e7312696919343387876c2dcf198cf08f395b Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Wed, 6 May 2026 02:34:38 +0900 Subject: [PATCH 07/13] Add social login uniqueness constraint --- .../oauth/google/GoogleLoginFilter.java | 12 +++++++++++- ...ate_social_users_add_unique_constraint.sql | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 ontime-back/src/main/resources/db/migration/V10__deduplicate_social_users_add_unique_constraint.sql diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java index 34833cb7..0c237eac 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java @@ -12,6 +12,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; @@ -58,7 +59,16 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ return googleLoginService.handleLogin(oAuthGoogleRequestDto, existingUsers.get(0), response); } else { OAuthGoogleUserDto oAuthGoogleUserDto = new OAuthGoogleUserDto(googleUserId, (String) googlePayload.get("name"), (String) googlePayload.get("picture"), googlePayload.getEmail()); - return googleLoginService.handleRegister(oAuthGoogleRequestDto, oAuthGoogleUserDto, response); + try { + return googleLoginService.handleRegister(oAuthGoogleRequestDto, oAuthGoogleUserDto, response); + } catch (DataIntegrityViolationException e) { + log.warn("Google 회원가입 중 중복 socialId가 감지되어 기존 계정으로 로그인합니다. socialId={}", googleUserId); + User user = userRepository.findAllBySocialTypeAndSocialIdOrderByIdDesc(SocialType.GOOGLE, googleUserId) + .stream() + .findFirst() + .orElseThrow(() -> e); + return googleLoginService.handleLogin(oAuthGoogleRequestDto, user, response); + } } } diff --git a/ontime-back/src/main/resources/db/migration/V10__deduplicate_social_users_add_unique_constraint.sql b/ontime-back/src/main/resources/db/migration/V10__deduplicate_social_users_add_unique_constraint.sql new file mode 100644 index 00000000..d22d6063 --- /dev/null +++ b/ontime-back/src/main/resources/db/migration/V10__deduplicate_social_users_add_unique_constraint.sql @@ -0,0 +1,19 @@ +CREATE TEMPORARY TABLE duplicate_social_user_keep AS +SELECT social_type, social_id, MAX(user_id) AS keep_user_id +FROM user +WHERE social_type IS NOT NULL + AND social_id IS NOT NULL +GROUP BY social_type, social_id +HAVING COUNT(*) > 1; + +DELETE u +FROM user u +JOIN duplicate_social_user_keep d + ON u.social_type = d.social_type + AND u.social_id = d.social_id +WHERE u.user_id <> d.keep_user_id; + +DROP TEMPORARY TABLE duplicate_social_user_keep; + +ALTER TABLE user +ADD CONSTRAINT uk_user_social_type_social_id UNIQUE (social_type, social_id); From 55a60986a44ad9951670e9056b91fddf8760e525 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Wed, 6 May 2026 02:40:11 +0900 Subject: [PATCH 08/13] Make social uniqueness migration idempotent --- ...ate_social_users_add_unique_constraint.sql | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/ontime-back/src/main/resources/db/migration/V10__deduplicate_social_users_add_unique_constraint.sql b/ontime-back/src/main/resources/db/migration/V10__deduplicate_social_users_add_unique_constraint.sql index d22d6063..c29388c5 100644 --- a/ontime-back/src/main/resources/db/migration/V10__deduplicate_social_users_add_unique_constraint.sql +++ b/ontime-back/src/main/resources/db/migration/V10__deduplicate_social_users_add_unique_constraint.sql @@ -1,19 +1,33 @@ -CREATE TEMPORARY TABLE duplicate_social_user_keep AS -SELECT social_type, social_id, MAX(user_id) AS keep_user_id -FROM user -WHERE social_type IS NOT NULL - AND social_id IS NOT NULL -GROUP BY social_type, social_id -HAVING COUNT(*) > 1; - DELETE u FROM user u -JOIN duplicate_social_user_keep d +JOIN ( + SELECT * FROM ( + SELECT social_type, social_id, MAX(user_id) AS keep_user_id + FROM user + WHERE social_type IS NOT NULL + AND social_id IS NOT NULL + GROUP BY social_type, social_id + HAVING COUNT(*) > 1 + ) duplicate_groups +) d ON u.social_type = d.social_type AND u.social_id = d.social_id WHERE u.user_id <> d.keep_user_id; -DROP TEMPORARY TABLE duplicate_social_user_keep; +SET @constraint_exists = ( + SELECT COUNT(*) + FROM information_schema.table_constraints + WHERE constraint_schema = DATABASE() + AND table_name = 'user' + AND constraint_name = 'uk_user_social_type_social_id' +); + +SET @add_constraint_sql = IF( + @constraint_exists = 0, + 'ALTER TABLE user ADD CONSTRAINT uk_user_social_type_social_id UNIQUE (social_type, social_id)', + 'SELECT 1' +); -ALTER TABLE user -ADD CONSTRAINT uk_user_social_type_social_id UNIQUE (social_type, social_id); +PREPARE add_constraint_statement FROM @add_constraint_sql; +EXECUTE add_constraint_statement; +DEALLOCATE PREPARE add_constraint_statement; From 89032ceae6f83315827950ac8a0c0ec2683a7768 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Wed, 6 May 2026 02:49:00 +0900 Subject: [PATCH 09/13] Harden social account deletion responses --- .../controller/SocialAuthController.java | 22 ++++++++++++++----- .../oauth/google/GoogleLoginService.java | 6 ++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java b/ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java index 163ae182..c4f687ba 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java +++ b/ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java @@ -6,6 +6,7 @@ import devkor.ontime_back.dto.OAuthKakaoUserDto; import devkor.ontime_back.global.oauth.apple.AppleLoginService; import devkor.ontime_back.global.oauth.google.GoogleLoginService; +import devkor.ontime_back.response.ApiResponseForm; import devkor.ontime_back.service.UserAuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -16,6 +17,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @Slf4j @@ -113,24 +115,32 @@ public String appleRegisterOrLogin(@RequestBody OAuthAppleRequestDto appleLoginR summary = "애플 소셜 로그인 회원탈퇴" ) @DeleteMapping("/apple/me") - public String appleDeleteUser(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) FeedbackAddDto feedbackAddDto) throws Exception { + public ResponseEntity> appleDeleteUser(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) FeedbackAddDto feedbackAddDto) { Long userId = userAuthService.getUserIdFromToken(request); log.info("userId: {}", userId); - appleLoginService.revokeToken(userId); + try { + appleLoginService.revokeToken(userId); + } catch (Exception e) { + log.warn("Apple 토큰 철회에 실패했지만 계정 삭제를 계속 진행합니다. userId={}, reason={}", userId, e.getMessage()); + } userAuthService.deleteUser(userId, feedbackAddDto); - return "애플 로그인 회원탈퇴 성공"; + return ResponseEntity.ok(ApiResponseForm.success(null, "애플 로그인 회원탈퇴 성공")); } @Operation( summary = "구글 소셜 로그인 회원탈퇴" ) @DeleteMapping("/google/me") - public String googleDeleteUser(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) FeedbackAddDto feedbackAddDto) throws Exception { + public ResponseEntity> googleDeleteUser(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) FeedbackAddDto feedbackAddDto) { Long userId = userAuthService.getUserIdFromToken(request); log.info("userId: {}", userId); - googleLoginService.revokeToken(userId); + try { + googleLoginService.revokeToken(userId); + } catch (Exception e) { + log.warn("Google 토큰 철회에 실패했지만 계정 삭제를 계속 진행합니다. userId={}, reason={}", userId, e.getMessage()); + } userAuthService.deleteUser(userId, feedbackAddDto); - return "구글 로그인 회원탈퇴 성공"; + return ResponseEntity.ok(ApiResponseForm.success(null, "구글 로그인 회원탈퇴 성공")); } diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java index 2be184bc..a2379e32 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java @@ -22,6 +22,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -175,7 +176,10 @@ public boolean revokeToken(Long userId) { String googleRefreshToken = user.getSocialLoginToken(); - RestTemplate restTemplate = new RestTemplate(); + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(3000); + requestFactory.setReadTimeout(3000); + RestTemplate restTemplate = new RestTemplate(requestFactory); String revokeUrl = GOOGLE_REVOKE_URL + googleRefreshToken; HttpHeaders headers = new HttpHeaders(); From 520d3e1f17ef0389be8f76f533d971b2e721d195 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Wed, 6 May 2026 12:11:20 +0900 Subject: [PATCH 10/13] Fix social login token storage --- .../src/main/java/devkor/ontime_back/entity/User.java | 4 +++- .../global/generallogin/handler/LoginSuccessHandler.java | 2 -- .../global/generallogin/service/LoginService.java | 4 +--- .../java/devkor/ontime_back/global/jwt/JwtTokenProvider.java | 5 ++--- .../ontime_back/global/oauth/apple/AppleLoginService.java | 1 - .../V9__change_field_social_login_token_longtext.sql | 2 ++ 6 files changed, 8 insertions(+), 10 deletions(-) create mode 100644 ontime-back/src/main/resources/db/migration/V9__change_field_social_login_token_longtext.sql diff --git a/ontime-back/src/main/java/devkor/ontime_back/entity/User.java b/ontime-back/src/main/java/devkor/ontime_back/entity/User.java index ac91e3df..ab15262a 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/entity/User.java +++ b/ontime-back/src/main/java/devkor/ontime_back/entity/User.java @@ -56,6 +56,8 @@ public class User { private String firebaseToken; + @Lob + @Column(columnDefinition = "LONGTEXT") private String socialLoginToken; @OneToOne(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL) @@ -134,4 +136,4 @@ public void updateFirebaseToken(String firebaseToken) { public void updateAccessToken(String accessToken) { this.accessToken = accessToken; } -} \ No newline at end of file +} diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/generallogin/handler/LoginSuccessHandler.java b/ontime-back/src/main/java/devkor/ontime_back/global/generallogin/handler/LoginSuccessHandler.java index 7b197647..fc5f18a1 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/generallogin/handler/LoginSuccessHandler.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/generallogin/handler/LoginSuccessHandler.java @@ -4,7 +4,6 @@ import devkor.ontime_back.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.hibernate.persister.entity.EntityNameUse; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; @@ -44,7 +43,6 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo userRepository.saveAndFlush(user); log.info("로그인에 성공하였습니다. 이메일 : {}", email); - log.info("로그인에 성공하였습니다. AccessToken : {}", accessToken); log.info("발급된 AccessToken 만료 기간 : {}", accessTokenExpiration); diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/generallogin/service/LoginService.java b/ontime-back/src/main/java/devkor/ontime_back/global/generallogin/service/LoginService.java index b0a2c5bd..e5d57692 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/generallogin/service/LoginService.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/generallogin/service/LoginService.java @@ -18,8 +18,6 @@ public class LoginService implements UserDetailsService { public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email) .orElseThrow(() -> new UsernameNotFoundException("해당 이메일이 존재하지 않습니다.")); - - System.out.println("유저임다: "+user); return org.springframework.security.core.userdetails.User.builder() .username(user.getEmail()) @@ -27,4 +25,4 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep .roles(user.getRole().name()) .build(); } -} \ No newline at end of file +} diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtTokenProvider.java b/ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtTokenProvider.java index d48df727..86caddf9 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtTokenProvider.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtTokenProvider.java @@ -79,7 +79,7 @@ public void sendAccessToken(HttpServletResponse response, String accessToken) { response.setStatus(HttpServletResponse.SC_OK); response.setHeader(accessHeader, accessToken); - log.info("발급된 Access Token : {}", accessToken); + log.info("Access Token 헤더 설정 완료"); } // accessToken + refreshToken header에 넣어서 전송 @@ -88,7 +88,6 @@ public void sendAccessAndRefreshToken(HttpServletResponse response, String acces setAccessTokenHeader(response, accessToken); setRefreshTokenHeader(response, refreshToken); - log.info("accesstoken: " + accessToken + "refreshtoken" + refreshToken); log.info("Access Token, Refresh Token 헤더 설정 완료"); } @@ -199,4 +198,4 @@ public String createExpiredAccessToken(String email) { .sign(Algorithm.HMAC512(secretKey)); } -} \ No newline at end of file +} diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginService.java b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginService.java index 15a3147b..d91fc028 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginService.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginService.java @@ -194,7 +194,6 @@ public AppleTokenResponseDto getAppleAccessTokenAndRefreshToken(String authCode) String clientSecret = generateClientSecret(); log.info("getAppleAccessTokenAndRefreshToken"); log.info("client_id: {}", clientId); - log.info("client_secret: {}", clientSecret); MultiValueMap requestBody = new LinkedMultiValueMap<>(); requestBody.add("grant_type", "authorization_code"); requestBody.add("code", authCode); diff --git a/ontime-back/src/main/resources/db/migration/V9__change_field_social_login_token_longtext.sql b/ontime-back/src/main/resources/db/migration/V9__change_field_social_login_token_longtext.sql new file mode 100644 index 00000000..cf797035 --- /dev/null +++ b/ontime-back/src/main/resources/db/migration/V9__change_field_social_login_token_longtext.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user` + MODIFY COLUMN `social_login_token` LONGTEXT NULL; From 4433a2eae2c7f66de7dbd8ca6828782b500bc780 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Wed, 6 May 2026 12:17:37 +0900 Subject: [PATCH 11/13] Fix Flyway migration version conflict --- ...text.sql => V11__change_field_social_login_token_longtext.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ontime-back/src/main/resources/db/migration/{V9__change_field_social_login_token_longtext.sql => V11__change_field_social_login_token_longtext.sql} (100%) diff --git a/ontime-back/src/main/resources/db/migration/V9__change_field_social_login_token_longtext.sql b/ontime-back/src/main/resources/db/migration/V11__change_field_social_login_token_longtext.sql similarity index 100% rename from ontime-back/src/main/resources/db/migration/V9__change_field_social_login_token_longtext.sql rename to ontime-back/src/main/resources/db/migration/V11__change_field_social_login_token_longtext.sql From f4b0ceb9cf565317a1635bcfda29d53ed3f8b178 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Wed, 6 May 2026 14:16:23 +0900 Subject: [PATCH 12/13] Fix Firebase initialization lifecycle hook --- .../devkor/ontime_back/config/FirebaseInitialization.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ontime-back/src/main/java/devkor/ontime_back/config/FirebaseInitialization.java b/ontime-back/src/main/java/devkor/ontime_back/config/FirebaseInitialization.java index 802468a5..fa8e0654 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/config/FirebaseInitialization.java +++ b/ontime-back/src/main/java/devkor/ontime_back/config/FirebaseInitialization.java @@ -7,7 +7,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import javax.annotation.PostConstruct; +import jakarta.annotation.PostConstruct; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -31,6 +31,9 @@ public void initialize() { if (FirebaseApp.getApps().isEmpty()) { FirebaseApp.initializeApp(options); + log.info("Firebase initialized successfully"); + } else { + log.info("Firebase already initialized"); } } catch (IOException e) { log.error("Failed to initialize Firebase", e); From 4d850f9db54fbd00e265f7f79a79ce1225ecd41c Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Thu, 7 May 2026 16:36:34 +0900 Subject: [PATCH 13/13] Harden production database configuration --- .github/workflows/deploy.yml | 50 +++++++-- .github/workflows/test.yml | 56 +--------- ontime-back/docs/database-configuration.md | 16 +++ .../resources/application-local.properties | 44 ++++++++ .../resources/application-prod.properties | 22 ++++ .../resources/application-test.properties | 48 +++++++++ .../DatabaseConfigurationPolicyTest.java | 102 ++++++++++++++++++ 7 files changed, 279 insertions(+), 59 deletions(-) create mode 100644 ontime-back/docs/database-configuration.md create mode 100644 ontime-back/src/main/resources/application-local.properties create mode 100644 ontime-back/src/main/resources/application-test.properties create mode 100644 ontime-back/src/test/java/devkor/ontime_back/security/DatabaseConfigurationPolicyTest.java diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5387dbef..7098287e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -93,14 +93,11 @@ jobs: SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }} SPRING_DATASOURCE_USERNAME=${{ secrets.SPRING_DATASOURCE_USERNAME }} SPRING_DATASOURCE_PASSWORD=${{ secrets.SPRING_DATASOURCE_PASSWORD }} - SPRING_DATASOURCE_DRIVER_CLASS_NAME=${{ secrets.SPRING_DATASOURCE_DRIVER_CLASS_NAME }} - SPRING_JPA_HIBERNATE_DDL_AUTO=${{ secrets.SPRING_JPA_HIBERNATE_DDL_AUTO }} + SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver + SPRING_JPA_HIBERNATE_DDL_AUTO=validate SPRING_FLYWAY_ENABLED=true - SPRING_FLYWAY_URL=${{ secrets.SPRING_FLYWAY_URL }} - SPRING_FLYWAY_USER=${{ secrets.SPRING_FLYWAY_USER }} - SPRING_FLYWAY_PASSWORD=${{ secrets.SPRING_FLYWAY_PASSWORD }} - SPRING_FLYWAY_BASELINE_ON_MIGRATE=true + SPRING_FLYWAY_BASELINE_ON_MIGRATE=false JWT_SECRET_KEY=${{ secrets.JWT_SECRETKEY }} JWT_ACCESS_EXPIRATION=${{ secrets.JWT_ACCESS_EXPIRATION }} @@ -139,6 +136,47 @@ jobs: FIREBASE_CREDENTIALS_BASE64=${{ secrets.FIREBASE_CREDENTIALS_BASE64 }} EOF + fail_deploy() { + echo "Unsafe production database configuration: $1" >&2 + exit 1 + } + + get_env_value() { + grep -E "^$1=" .env | tail -n 1 | cut -d= -f2- + } + + DB_URL="$(get_env_value SPRING_DATASOURCE_URL)" + DB_USERNAME="$(get_env_value SPRING_DATASOURCE_USERNAME)" + DB_PASSWORD="$(get_env_value SPRING_DATASOURCE_PASSWORD)" + DDL_AUTO="$(get_env_value SPRING_JPA_HIBERNATE_DDL_AUTO)" + FLYWAY_BASELINE="$(get_env_value SPRING_FLYWAY_BASELINE_ON_MIGRATE)" + NORMALIZED_DB_URL="$(printf '%s' "$DB_URL" | tr '[:upper:]' '[:lower:]')" + NORMALIZED_DB_USERNAME="$(printf '%s' "$DB_USERNAME" | tr '[:upper:]' '[:lower:]')" + + [ -n "$DB_URL" ] || fail_deploy "SPRING_DATASOURCE_URL is required." + [ -n "$DB_USERNAME" ] || fail_deploy "SPRING_DATASOURCE_USERNAME is required." + [ -n "$DB_PASSWORD" ] || fail_deploy "SPRING_DATASOURCE_PASSWORD is required." + [ "$NORMALIZED_DB_USERNAME" != "root" ] || fail_deploy "SPRING_DATASOURCE_USERNAME must not be root." + [ "$DDL_AUTO" = "validate" ] || fail_deploy "SPRING_JPA_HIBERNATE_DDL_AUTO must be validate." + [ "$FLYWAY_BASELINE" = "false" ] || fail_deploy "SPRING_FLYWAY_BASELINE_ON_MIGRATE must be false." + + case "$NORMALIZED_DB_URL" in + *allowpublickeyretrieval=true*) fail_deploy "SPRING_DATASOURCE_URL must not enable allowPublicKeyRetrieval." ;; + esac + + case "$NORMALIZED_DB_URL" in + *createdatabaseifnotexist=true*) fail_deploy "SPRING_DATASOURCE_URL must not create databases at startup." ;; + esac + + case "$NORMALIZED_DB_URL" in + *usessl=false*) fail_deploy "SPRING_DATASOURCE_URL must not disable TLS." ;; + esac + + case "$NORMALIZED_DB_URL" in + *sslmode=required*|*sslmode=verify_ca*|*sslmode=verify_identity*) ;; + *) fail_deploy "SPRING_DATASOURCE_URL must declare sslMode=REQUIRED, VERIFY_CA, or VERIFY_IDENTITY." ;; + esac + echo "${{ secrets.GHCR_READ_TOKEN }}" | sudo docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin if sudo docker compose version >/dev/null 2>&1; then diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf353db3..b2489e33 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,62 +62,12 @@ jobs: - # 4. 환경 변수 설정 파일 생성 - - name: Create Config Files - run: | - mkdir -p ontime-back/src/main/resources - mkdir -p ontime-back/src/main/resources/key - echo "spring.application.name=${{ secrets.SPRING_APPLICATION_NAME }}" > ontime-back/src/main/resources/application.properties - echo "spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test_db?serverTimezone=UTC&useSSL=false" >> ontime-back/src/main/resources/application.properties - echo "spring.datasource.username=test_user" >> ontime-back/src/main/resources/application.properties - echo "spring.datasource.password=test_password" >> ontime-back/src/main/resources/application.properties - echo "spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver" >> ontime-back/src/main/resources/application.properties - echo "spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect" >> ontime-back/src/main/resources/application.properties - echo "spring.jpa.hibernate.ddl-auto=validate" >> ontime-back/src/main/resources/application.properties - echo "spring.sql.init.mode=always" >> ontime-back/src/main/resources/application.properties - echo "jwt.secret.key=${{ secrets.JWT_SECRETKEY }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.access.expiration=${{ secrets.JWT_ACCESS_EXPIRATION }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.refresh.expiration=${{ secrets.JWT_REFRESH_EXPIRATION }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.access.header=${{ secrets.JWT_ACCESS_HEADER }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.refresh.header=${{ secrets.JWT_REFRESH_HEADER }}" >> ontime-back/src/main/resources/application.properties - echo "google.web.client-id = ${{ secrets.GOOGLE_WEB_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties - echo "google.app.client-id = ${{ secrets.GOOGLE_APP_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.client-secret=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.scope=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_SCOPE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.redirect-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_REDIRECT_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.authorization-grant-type=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_AUTHORIZATION_GRANT_TYPE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.client-name=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_NAME }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.google.authorization-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_AUTHORIZATION_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.google.token-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_TOKEN_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.google.user-info-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_INFO_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.google.user-name-attribute=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_NAME_ATTRIBUTE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.client-id=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.scope=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_SCOPE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.redirect-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_REDIRECT_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.authorization-grant-type=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_AUTHORIZATION_GRANT_TYPE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.client-name=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_NAME }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.kakao.authorization-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_AUTHORIZATION_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.kakao.token-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_TOKEN_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.kakao.user-info-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_INFO_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.kakao.user-name-attribute=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_NAME_ATTRIBUTE }}" >> ontime-back/src/main/resources/application.properties - echo "apple.client.id=${{ secrets.APPLE_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties - echo "apple.client.secret=${{ secrets.APPLE_CLIENT_SECRET }}" >> ontime-back/src/main/resources/application.properties - echo "apple.login.key=${{ secrets.APPLE_LOGIN_KEY }}" >> ontime-back/src/main/resources/application.properties - echo "apple.team.id=${{ secrets.APPLE_TEAM_ID }}" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.enabled=true" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.url=jdbc:mysql://127.0.0.1:3306/test_db?serverTimezone=UTC&useSSL=false" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.user=test_user" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.password=test_password" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.baseline-on-migrate=true" >> ontime-back/src/main/resources/application.properties - echo "management.endpoints.web.exposure.include=health" >> ontime-back/src/main/resources/application.properties - echo "management.endpoint.health.show-details=always" >> ontime-back/src/main/resources/application.properties - echo "${{ secrets.ONTIME_PUSH_FIREBASE_ADMINSDK }}" > ontime-back/src/main/resources/ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json - echo "${{ secrets.AUTHKEY_743M7R5W3W }}" > ontime-back/src/main/resources/key/AuthKey_743M7R5W3W.p8 - - # 5. Gradle 빌드 & JUnit 테스트 실행 + # 4. Gradle 빌드 & JUnit 테스트 실행 - name: Run Tests with Gradle id: run-tests # 실행 결과를 output으로 저장할 id 추가 continue-on-error: true + env: + SPRING_PROFILES_ACTIVE: test run: | cd ontime-back ./gradlew test diff --git a/ontime-back/docs/database-configuration.md b/ontime-back/docs/database-configuration.md new file mode 100644 index 00000000..b7e63517 --- /dev/null +++ b/ontime-back/docs/database-configuration.md @@ -0,0 +1,16 @@ +# Database Configuration + +Production uses MySQL with environment-provided database credentials. + +## Production + +- Spring profile: `prod` +- Datasource URL: `SPRING_DATASOURCE_URL` +- Datasource username: `SPRING_DATASOURCE_USERNAME` +- Datasource password: `SPRING_DATASOURCE_PASSWORD` +- Datasource driver: `com.mysql.cj.jdbc.Driver` +- Hibernate DDL mode: `validate` +- SQL logging: disabled +- Formatted SQL logging: disabled +- Flyway: enabled +- Flyway baseline on migrate: disabled diff --git a/ontime-back/src/main/resources/application-local.properties b/ontime-back/src/main/resources/application-local.properties new file mode 100644 index 00000000..a8813db0 --- /dev/null +++ b/ontime-back/src/main/resources/application-local.properties @@ -0,0 +1,44 @@ +# Database Configuration +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/ontime_db?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:root} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:ontime1234} +spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver} + +# JPA / Hibernate +spring.jpa.database=mysql +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# Flyway +spring.flyway.enabled=true +spring.flyway.baseline-on-migrate=true + +# JWT Configuration +jwt.secret.key=${JWT_SECRET_KEY:my_secret_key_for_ontime_back_application_development_environment_1234567890} +jwt.access.expiration=${JWT_ACCESS_EXPIRATION:3600000} +jwt.refresh.expiration=${JWT_REFRESH_EXPIRATION:1209600000} +jwt.access.header=${JWT_ACCESS_HEADER:Authorization} +jwt.refresh.header=${JWT_REFRESH_HEADER:Authorization-refresh} + +# Google OAuth +google.web.client-id=${GOOGLE_WEB_CLIENT_ID:599377893328-dljp16andl10374bnm9b1nnfp9uj5pvd.apps.googleusercontent.com} +google.app.client-id=${GOOGLE_APP_CLIENT_ID:456571312261-r35ah9qi0qaq7al007e2db0e0jmjcmb4.apps.googleusercontent.com} + +# Apple OAuth +apple.client.id=${APPLE_CLIENT_ID:your_apple_client_id} +apple.team.id=${APPLE_TEAM_ID:your_apple_team_id} +apple.login.key=${APPLE_LOGIN_KEY:your_apple_key_id} +apple.client.secret=${APPLE_CLIENT_SECRET:your_apple_private_key} +apple.private-key.base64=${APPLE_PRIVATE_KEY_BASE64:} + +# Firebase +firebase.credentials.base64=${FIREBASE_CREDENTIALS_BASE64:} + +# Logging +logging.level.root=INFO +logging.level.devkor.ontime_back=DEBUG + +# Feature flags +feature.apple-login.enabled=${FEATURE_APPLE_LOGIN_ENABLED:true} diff --git a/ontime-back/src/main/resources/application-prod.properties b/ontime-back/src/main/resources/application-prod.properties index 851a6410..cd67d8b7 100644 --- a/ontime-back/src/main/resources/application-prod.properties +++ b/ontime-back/src/main/resources/application-prod.properties @@ -1,3 +1,25 @@ +# Database Configuration +spring.datasource.url=${SPRING_DATASOURCE_URL} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} +spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver} + +# JPA / Hibernate +spring.jpa.database=mysql +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.show-sql=false +spring.jpa.properties.hibernate.format_sql=false + +# Flyway +spring.flyway.enabled=true +spring.flyway.baseline-on-migrate=false + +# Logging +logging.level.root=INFO +logging.level.devkor.ontime_back=INFO + +# Actuator management.endpoint.health.probes.enabled=true management.endpoints.web.exposure.include=health management.health.readinessstate.enabled=true diff --git a/ontime-back/src/main/resources/application-test.properties b/ontime-back/src/main/resources/application-test.properties new file mode 100644 index 00000000..b2e10819 --- /dev/null +++ b/ontime-back/src/main/resources/application-test.properties @@ -0,0 +1,48 @@ +# Database Configuration +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:mysql://127.0.0.1:3306/test_db?serverTimezone=UTC&useSSL=false} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:test_user} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:test_password} +spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver} + +# JPA / Hibernate +spring.jpa.database=mysql +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.show-sql=false +spring.jpa.properties.hibernate.format_sql=false + +# Flyway +spring.flyway.enabled=true +spring.flyway.baseline-on-migrate=false + +# JWT Configuration +jwt.secret.key=${JWT_SECRET_KEY:test_secret_key_for_ontime_back_application_tests_1234567890} +jwt.access.expiration=${JWT_ACCESS_EXPIRATION:3600000} +jwt.refresh.expiration=${JWT_REFRESH_EXPIRATION:1209600000} +jwt.access.header=${JWT_ACCESS_HEADER:Authorization} +jwt.refresh.header=${JWT_REFRESH_HEADER:Authorization-refresh} + +# Google OAuth +google.web.client-id=${GOOGLE_WEB_CLIENT_ID:test-google-web-client-id} +google.app.client-id=${GOOGLE_APP_CLIENT_ID:test-google-app-client-id} + +# Apple OAuth +apple.client.id=${APPLE_CLIENT_ID:test-apple-client-id} +apple.team.id=${APPLE_TEAM_ID:test-apple-team-id} +apple.login.key=${APPLE_LOGIN_KEY:test-apple-key-id} +apple.client.secret=${APPLE_CLIENT_SECRET:} +apple.private-key.base64=${APPLE_PRIVATE_KEY_BASE64:} + +# Firebase +firebase.credentials.base64=${FIREBASE_CREDENTIALS_BASE64:} + +# Logging +logging.level.root=INFO +logging.level.devkor.ontime_back=INFO + +# Feature flags +feature.apple-login.enabled=${FEATURE_APPLE_LOGIN_ENABLED:false} + +# Actuator +management.endpoints.web.exposure.include=health +management.endpoint.health.show-details=always diff --git a/ontime-back/src/test/java/devkor/ontime_back/security/DatabaseConfigurationPolicyTest.java b/ontime-back/src/test/java/devkor/ontime_back/security/DatabaseConfigurationPolicyTest.java new file mode 100644 index 00000000..25c88de2 --- /dev/null +++ b/ontime-back/src/test/java/devkor/ontime_back/security/DatabaseConfigurationPolicyTest.java @@ -0,0 +1,102 @@ +package devkor.ontime_back.security; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; + +class DatabaseConfigurationPolicyTest { + + private static final Path RESOURCES_ROOT = Path.of("src/main/resources"); + + @Test + void productionLikeProfilesDoNotMutateSchemaOrLogSql() throws IOException { + Properties properties = loadProperties("application-prod.properties"); + + assertThat(properties) + .as("prod profile database safety") + .containsEntry("spring.datasource.url", "${SPRING_DATASOURCE_URL}") + .containsEntry("spring.datasource.username", "${SPRING_DATASOURCE_USERNAME}") + .containsEntry("spring.datasource.password", "${SPRING_DATASOURCE_PASSWORD}") + .containsEntry("spring.jpa.hibernate.ddl-auto", "validate") + .containsEntry("spring.jpa.show-sql", "false") + .containsEntry("spring.jpa.properties.hibernate.format_sql", "false") + .containsEntry("spring.flyway.enabled", "true") + .containsEntry("spring.flyway.baseline-on-migrate", "false"); + } + + @Test + void productionLikeProfilesDoNotContainFallbackCredentialsOrUnsafeJdbcFlags() throws IOException { + String content = Files.readString(RESOURCES_ROOT.resolve("application-prod.properties")); + String normalizedContent = content.toLowerCase(Locale.ROOT); + + assertThat(normalizedContent) + .as("prod profile must not include unsafe production defaults") + .doesNotContain(":root") + .doesNotContain("ontime1234") + .doesNotContain("allowpublickeyretrieval=true") + .doesNotContain("createdatabaseifnotexist=true") + .doesNotContain("usessl=false"); + } + + @Test + void localProfileKeepsDeveloperOnlySchemaUpdateDefaults() throws IOException { + Properties properties = loadProperties("application-local.properties"); + + assertThat(properties) + .containsEntry("spring.jpa.hibernate.ddl-auto", "update") + .containsEntry("spring.jpa.show-sql", "true") + .containsEntry("spring.jpa.properties.hibernate.format_sql", "true"); + } + + @Test + void deployWorkflowPinsAndValidatesProductionDatabaseSafety() throws IOException { + String workflow = Files.readString(repoRoot().resolve(".github/workflows/deploy.yml")); + + assertThat(workflow) + .contains("SPRING_JPA_HIBERNATE_DDL_AUTO=validate") + .contains("SPRING_FLYWAY_BASELINE_ON_MIGRATE=false") + .contains("SPRING_DATASOURCE_URL is required.") + .contains("SPRING_DATASOURCE_USERNAME is required.") + .contains("SPRING_DATASOURCE_PASSWORD is required.") + .contains("SPRING_DATASOURCE_USERNAME must not be root.") + .contains("allowpublickeyretrieval=true") + .contains("createdatabaseifnotexist=true") + .contains("usessl=false") + .contains("sslmode=required") + .doesNotContain("SPRING_JPA_HIBERNATE_DDL_AUTO=${{ secrets.SPRING_JPA_HIBERNATE_DDL_AUTO }}") + .doesNotContain("SPRING_FLYWAY_BASELINE_ON_MIGRATE=true"); + } + + @Test + void testWorkflowUsesTrackedTestProfileInsteadOfGeneratingIgnoredApplicationProperties() throws IOException { + String workflow = Files.readString(repoRoot().resolve(".github/workflows/test.yml")); + + assertThat(workflow) + .contains("SPRING_PROFILES_ACTIVE: test") + .doesNotContain("Create Config Files") + .doesNotContain("ontime-back/src/main/resources/application.properties"); + } + + private Properties loadProperties(String fileName) throws IOException { + Properties properties = new Properties(); + try (InputStream inputStream = Files.newInputStream(RESOURCES_ROOT.resolve(fileName))) { + properties.load(inputStream); + } + return properties; + } + + private Path repoRoot() { + Path current = Path.of("").toAbsolutePath(); + if (Files.exists(current.resolve(".github/workflows/deploy.yml"))) { + return current; + } + return current.getParent(); + } +}