diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6bd862c9..5387dbef 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,159 +1,163 @@ name: Deploy on: - workflow_dispatch: push: branches: - deploy +permissions: + contents: read + packages: write + +concurrency: + group: deploy-production + cancel-in-progress: false + +env: + REGISTRY: ghcr.io + IMAGE_NAME: devkor-github/ontime-back + IMAGE_TAG: ${{ github.sha }} + jobs: - deploy-to-ec2: + build-and-push: runs-on: ubuntu-latest - steps: - - name: Checkout code + - name: Checkout repository uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push image + uses: docker/build-push-action@v6 with: - java-version: '17' - distribution: 'temurin' - - - name: Build with Gradle - run: | - cd ontime-back - ./gradlew build -x test - - - 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: | - 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 + context: ./ontime-back + file: ./ontime-back/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:deploy-latest + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy-to-ec2: + needs: build-and-push + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Upload compose file 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,Dockerfile,docker-compose.yml,config/application.properties,secrets/firebase-adminsdk.json,secrets/AuthKey_743M7R5W3W.p8" + source: "ontime-back/docker-compose.yml" target: "/home/ubuntu/OnTime-back" + strip_components: 1 - - name: Restart service on EC2 - uses: appleboy/ssh-action@master + - name: Pull image and restart container + uses: appleboy/ssh-action@v1.0.3 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 + set -eu + + DEPLOY_DIR="/home/ubuntu/OnTime-back" + CONTAINER_NAME="ontime-container" + + mkdir -p "$DEPLOY_DIR" + cd "$DEPLOY_DIR" + + umask 077 + cat > .env <<'EOF' + IMAGE_TAG=${{ env.IMAGE_TAG }} + BACKEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + BACKEND_CONTAINER_NAME=ontime-container + BACKEND_HTTP_PORT=${{ secrets.BACKEND_HTTP_PORT || '8080' }} + SERVER_PORT=8080 + SPRING_PROFILES_ACTIVE=prod + JAVA_TOOL_OPTIONS=-XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom + + 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_HIBERNATE_DDL_AUTO=${{ secrets.SPRING_JPA_HIBERNATE_DDL_AUTO }} + + 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 + + JWT_SECRET_KEY=${{ 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_TEAM_ID=${{ secrets.APPLE_TEAM_ID }} + APPLE_LOGIN_KEY=${{ secrets.APPLE_LOGIN_KEY }} + APPLE_PRIVATE_KEY_BASE64=${{ secrets.APPLE_PRIVATE_KEY_BASE64 }} + FEATURE_APPLE_LOGIN_ENABLED=${{ secrets.FEATURE_APPLE_LOGIN_ENABLED || 'true' }} + + FIREBASE_CREDENTIALS_BASE64=${{ secrets.FIREBASE_CREDENTIALS_BASE64 }} + EOF + + 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 - sudo docker compose down - sudo docker compose up --build -d + sudo docker compose pull + sudo docker compose up -d --remove-orphans else - sudo docker-compose down - sudo docker-compose up --build -d + sudo docker-compose pull + sudo docker-compose up -d --remove-orphans fi - sudo docker image prune -f + + for attempt in $(seq 1 30); do + STATUS="$(sudo docker inspect -f '{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null || true)" + if [ "$STATUS" = "healthy" ]; then + echo "Container is healthy." + exit 0 + fi + echo "Waiting for healthy container status; current status: ${STATUS:-unknown}" + sleep 5 + done + + sudo docker logs --tail=200 "$CONTAINER_NAME" || true + exit 1 diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 00000000..c8eb53dd --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,121 @@ +# Production Deployment + +This service deploys as an immutable Docker image published to GitHub Container Registry (GHCR). Runtime configuration is injected through the EC2 `.env` file generated by GitHub Actions; private resource files are not copied into the image or bind-mounted from the host. + +## Required GitHub Secrets + +Deployment access: + +- `EC2_HOST` +- `EC2_USER` +- `EC2_SSH_KEY` +- `GHCR_USERNAME` +- `GHCR_READ_TOKEN` + +Runtime image and port: + +- `BACKEND_HTTP_PORT` (optional, defaults to `8080`) + +Spring and database: + +- `SPRING_APPLICATION_NAME` +- `SPRING_DATASOURCE_URL` +- `SPRING_DATASOURCE_USERNAME` +- `SPRING_DATASOURCE_PASSWORD` +- `SPRING_DATASOURCE_DRIVER_CLASS_NAME` +- `SPRING_JPA_HIBERNATE_DDL_AUTO` +- `SPRING_FLYWAY_URL` +- `SPRING_FLYWAY_USER` +- `SPRING_FLYWAY_PASSWORD` + +Authentication and OAuth: + +- `JWT_SECRETKEY` +- `JWT_ACCESS_EXPIRATION` +- `JWT_REFRESH_EXPIRATION` +- `JWT_ACCESS_HEADER` +- `JWT_REFRESH_HEADER` +- `GOOGLE_WEB_CLIENT_ID` +- `GOOGLE_APP_CLIENT_ID` +- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET` +- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_SCOPE` +- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_REDIRECT_URI` +- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_AUTHORIZATION_GRANT_TYPE` +- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_NAME` +- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_AUTHORIZATION_URI` +- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_TOKEN_URI` +- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_INFO_URI` +- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_NAME_ATTRIBUTE` +- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID` +- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_SCOPE` +- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_REDIRECT_URI` +- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_AUTHORIZATION_GRANT_TYPE` +- `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_NAME` +- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_AUTHORIZATION_URI` +- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_TOKEN_URI` +- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_INFO_URI` +- `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_NAME_ATTRIBUTE` +- `APPLE_CLIENT_ID` +- `APPLE_TEAM_ID` +- `APPLE_LOGIN_KEY` +- `APPLE_PRIVATE_KEY_BASE64` +- `FEATURE_APPLE_LOGIN_ENABLED` (optional, defaults to `true`) + +Firebase: + +- `FIREBASE_CREDENTIALS_BASE64` + +Set the base64 secrets from the ignored local credential files: + +```bash +base64 -i ontime-back/src/main/resources/ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json | tr -d '\n' | gh secret set FIREBASE_CREDENTIALS_BASE64 --repo DevKor-github/OnTime-back +``` + +```bash +base64 -i ontime-back/src/main/resources/key/AuthKey_743M7R5W3W.p8 | tr -d '\n' | gh secret set APPLE_PRIVATE_KEY_BASE64 --repo DevKor-github/OnTime-back +``` + +## Build And Release Flow + +Push to the `deploy` branch to trigger `.github/workflows/deploy.yml`. + +The workflow: + +1. Builds `ontime-back/Dockerfile` from the `ontime-back/` context. +2. Pushes two GHCR tags: + - `ghcr.io/devkor-github/ontime-back:` + - `ghcr.io/devkor-github/ontime-back:deploy-latest` +3. Uploads `docker-compose.yml` to `/home/ubuntu/OnTime-back`. +4. Writes `/home/ubuntu/OnTime-back/.env` from GitHub secrets. +5. Runs `docker compose pull && docker compose up -d --remove-orphans`. +6. Waits until the `ontime-container` Docker health status is `healthy`. + +## Health Verification + +The production image exposes a Docker healthcheck against: + +```text +/actuator/health/readiness +``` + +Manual checks on EC2: + +```bash +cd /home/ubuntu/OnTime-back +sudo docker compose ps +sudo docker inspect -f '{{.State.Health.Status}}' ontime-container +curl -fsS http://localhost:8080/actuator/health/readiness +``` + +## Rollback + +Every deploy is tagged by commit SHA. To roll back, set `IMAGE_TAG` in `/home/ubuntu/OnTime-back/.env` to the previous known-good SHA, then restart from the existing Compose file: + +```bash +cd /home/ubuntu/OnTime-back +sudo docker compose pull +sudo docker compose up -d --remove-orphans +sudo docker inspect -f '{{.State.Health.Status}}' ontime-container +``` + +Keep the previous SHA in the release notes or GitHub Actions deploy history so rollback does not depend on `deploy-latest`. diff --git a/ontime-back/.dockerignore b/ontime-back/.dockerignore new file mode 100644 index 00000000..40b0eaf1 --- /dev/null +++ b/ontime-back/.dockerignore @@ -0,0 +1,21 @@ +.env +.git +.gitignore +.gradle +build +out +bin +.idea +.vscode +*.iml +*.iws +*.ipr +.DS_Store + +src/main/resources/application.properties +src/main/resources/*.json +src/main/resources/key +src/main/resources/**/*.p8 + +Dockerfile +docker-compose*.yml diff --git a/ontime-back/Dockerfile b/ontime-back/Dockerfile index 213213e0..23f81c6f 100644 --- a/ontime-back/Dockerfile +++ b/ontime-back/Dockerfile @@ -1,10 +1,40 @@ -FROM eclipse-temurin:17-jre -RUN apt-get update && \ - apt-get install -y tzdata && \ - ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \ - echo "Asia/Seoul" > /etc/timezone && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* +FROM eclipse-temurin:17-jdk-jammy AS builder + +WORKDIR /workspace + +COPY gradlew build.gradle settings.gradle ./ +COPY gradle ./gradle +RUN chmod +x ./gradlew + +COPY src ./src +RUN ./gradlew clean bootJar --no-daemon + +FROM eclipse-temurin:17-jre-jammy + +ENV TZ=Asia/Seoul \ + LANG=C.UTF-8 \ + SPRING_PROFILES_ACTIVE=prod \ + JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom" + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl tzdata \ + && ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ + && echo "${TZ}" > /etc/timezone \ + && groupadd --system app \ + && useradd --system --gid app --home-dir /app --shell /usr/sbin/nologin app \ + && mkdir -p /app \ + && chown -R app:app /app \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /app -COPY project.jar app.jar +COPY --from=builder --chown=app:app /workspace/build/libs/*.jar /app/app.jar + +USER app +EXPOSE 8080 +STOPSIGNAL SIGTERM + +HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ + CMD curl -fsS "http://localhost:${SERVER_PORT:-8080}/actuator/health/readiness" || exit 1 + ENTRYPOINT ["java", "-jar", "/app/app.jar"] diff --git a/ontime-back/docker-compose.yml b/ontime-back/docker-compose.yml index 81cd810d..e21529d8 100644 --- a/ontime-back/docker-compose.yml +++ b/ontime-back/docker-compose.yml @@ -1,14 +1,22 @@ services: backend: - build: - context: . - dockerfile: Dockerfile - image: ontime-backend - container_name: ontime-backend - restart: unless-stopped + image: "${BACKEND_IMAGE:-ghcr.io/devkor-github/ontime-back}:${IMAGE_TAG:?IMAGE_TAG is required}" + container_name: "${BACKEND_CONTAINER_NAME:-ontime-container}" + env_file: + - .env + environment: + SPRING_PROFILES_ACTIVE: "${SPRING_PROFILES_ACTIVE:-prod}" + SERVER_PORT: "${SERVER_PORT:-8080}" + JAVA_TOOL_OPTIONS: "${JAVA_TOOL_OPTIONS:--XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom}" 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 + - "${BACKEND_HTTP_PORT:-8080}:${SERVER_PORT:-8080}" + restart: unless-stopped + stop_grace_period: 30s + mem_limit: "${BACKEND_MEMORY_LIMIT:-768m}" + cpus: "${BACKEND_CPU_LIMIT:-1.0}" + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:${SERVER_PORT:-8080}/actuator/health/readiness || exit 1"] + interval: 30s + timeout: 5s + start_period: 60s + retries: 3 diff --git a/ontime-back/docs/logging-redaction-policy.md b/ontime-back/docs/logging-redaction-policy.md new file mode 100644 index 00000000..640bcc28 --- /dev/null +++ b/ontime-back/docs/logging-redaction-policy.md @@ -0,0 +1,40 @@ +# Logging Redaction Policy + +Production logs must only contain operational metadata needed to debug routing, +ownership, status, and latency. Request payloads are not safe log data. + +## Request Logs + +Controller request logging is centralized in `LoggingAspect` and +`RequestLogPolicy`. Request logs must include only: + +- request ID +- route +- method +- actor identifier +- client IP +- response status +- timing in milliseconds + +The request logger must not inspect or render `@RequestBody` arguments. + +## Sensitive Fields + +Never log values or raw key/value payloads for credentials, OAuth material, +profile text, notes, or request bodies. Sensitive key names include: + +- `authorization` +- `firebaseToken` +- `password` +- `secret` +- `token` + +When field-level logging is genuinely needed, add the field to +`RequestLogPolicy`'s allowlist and keep the log statement metadata-only. + +## Regression Guard + +`SensitiveLoggingPolicyTest` scans application source for sensitive key names in +logger calls, request-body logging in `LoggingAspect`, and DTO-generated +`toString` methods. Any future exception must update this policy and the +central allowlist in the same change. diff --git a/ontime-back/src/main/java/devkor/ontime_back/LoggingAspect.java b/ontime-back/src/main/java/devkor/ontime_back/LoggingAspect.java index a3873b74..ab72d19d 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/LoggingAspect.java +++ b/ontime-back/src/main/java/devkor/ontime_back/LoggingAspect.java @@ -2,34 +2,23 @@ import devkor.ontime_back.dto.RequestInfoDto; import devkor.ontime_back.entity.ApiLog; -import devkor.ontime_back.repository.ApiLogRepository; +import devkor.ontime_back.logging.RequestLogPolicy; import devkor.ontime_back.response.GeneralException; import devkor.ontime_back.service.ApiLogService; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; -import org.aspectj.lang.reflect.MethodSignature; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import java.lang.annotation.Annotation; -import java.util.Map; - - @Slf4j @Aspect @Component @@ -37,95 +26,47 @@ public class LoggingAspect { private final ApiLogService apiLogService; - private static final String NO_PARAMS = "No Params"; - private static final String NO_BODY = "No Body"; @Pointcut("bean(*Controller)") private void allRequest() {} @Around("allRequest()") public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable { - RequestInfoDto requestInfoDto = extractRequestInfo(); - - // requestTime + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpServletRequest request = attributes.getRequest(); + String requestId = RequestLogPolicy.resolveRequestId(request); + RequestLogPolicy.exposeRequestId(attributes, requestId); + RequestInfoDto requestInfoDto = extractRequestInfo(request); long beforeRequest = System.currentTimeMillis(); - // pathVariable, requestBody - MethodSignature signature = (MethodSignature) joinPoint.getSignature(); - Object[] args = joinPoint.getArgs(); - Annotation[][] parameterAnnotations = signature.getMethod().getParameterAnnotations(); - - String pathVariable = null; - String requestBody = null; - - for (int i = 0; i < parameterAnnotations.length; i++) { - Annotation[] annotations = parameterAnnotations[i]; - for (Annotation annotation : annotations) { - if (annotation instanceof PathVariable) { - pathVariable = args[i].toString(); // @PathVariable 값 저장 - } else if (annotation instanceof RequestBody) { - requestBody = args[i].toString(); // @RequestBody 값 저장 - } - } - } - - // responseStatus - int responseStatus = 200; - Object result; try { - // 실제 메서드 실행 - result = joinPoint.proceed(); + Object result = joinPoint.proceed(); + int responseStatus = 200; if (result instanceof ResponseEntity) { ResponseEntity responseEntity = (ResponseEntity) result; - responseStatus = responseEntity.getStatusCodeValue(); // 상태 코드 추출 + responseStatus = responseEntity.getStatusCode().value(); } - // 정상 요청 로그 저장 long timeTaken = System.currentTimeMillis() - beforeRequest; - - ApiLog apiLog = buildApiLog(requestInfoDto, responseStatus, timeTaken); - apiLogService.saveLog(apiLog); - - log.info("[Request Log] requestUrl: {}, requestMethod: {}, userId: {}, clientIp: {}, pathVariable: {}, requestBody: {}, responseStatus: {}, timeTaken: {}", - requestInfoDto.getRequestUrl(), requestInfoDto.getRequestMethod(), requestInfoDto.getUserId(), requestInfoDto.getClientIp(), - pathVariable != null ? pathVariable : NO_PARAMS, - requestBody != null ? requestBody : NO_BODY, - responseStatus, timeTaken); + saveApiLog(requestInfoDto, responseStatus, timeTaken); + log.info("[Request Log] requestId: {}, route: {}, method: {}, actor: {}, clientIp: {}, responseStatus: {}, timeTakenMs: {}", + requestId, requestInfoDto.getRequestUrl(), requestInfoDto.getRequestMethod(), requestInfoDto.getUserId(), + requestInfoDto.getClientIp(), responseStatus, timeTaken); return result; - - } catch (Exception ex) { + } catch (Throwable ex) { + int responseStatus = mapExceptionToStatusCode(ex); + long timeTaken = System.currentTimeMillis() - beforeRequest; + saveApiLog(requestInfoDto, responseStatus, timeTaken); + log.error("[Error Log] requestId: {}, route: {}, method: {}, actor: {}, clientIp: {}, exception: {}, responseStatus: {}, timeTakenMs: {}", + requestId, requestInfoDto.getRequestUrl(), requestInfoDto.getRequestMethod(), requestInfoDto.getUserId(), + requestInfoDto.getClientIp(), ex.getClass().getSimpleName(), responseStatus, timeTaken); throw ex; } } - @AfterThrowing(pointcut = "allRequest()", throwing = "ex") - public void logException(JoinPoint joinPoint, Exception ex) { - RequestInfoDto requestInfoDto = extractRequestInfo(); - - // exceptionName - String exceptionName; - if (ex instanceof GeneralException) { - exceptionName = ((GeneralException) ex).getErrorCode().name(); - } else { - exceptionName = ex.getClass().getSimpleName(); - }; - // exceptionMessage - String exceptionMessage = ex.getMessage(); - // responseStatus - int responseStatus = mapExceptionToStatusCode(ex); - - log.error("[Error Log] requestUrl: {}, requestMethod: {}, userId: {}, clientIp: {}, exception: {}, message: {}, responseStatus: {}", - requestInfoDto.getRequestUrl(), requestInfoDto.getRequestMethod(), requestInfoDto.getUserId(), requestInfoDto.getClientIp(), exceptionName, exceptionMessage, responseStatus); - - ApiLog errorLog = buildApiLog(requestInfoDto, responseStatus, 0); - apiLogService.saveLog(errorLog); - } - // requestinfo 추출 - private RequestInfoDto extractRequestInfo() { - HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); - + private RequestInfoDto extractRequestInfo(HttpServletRequest request) { String requestUrl = request.getRequestURI(); String requestMethod = request.getMethod(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); @@ -137,6 +78,11 @@ private RequestInfoDto extractRequestInfo() { return new RequestInfoDto(requestUrl, requestMethod, userId, clientIp); } + private void saveApiLog(RequestInfoDto requestInfoDto, int responseStatus, long timeTaken) { + ApiLog apiLog = buildApiLog(requestInfoDto, responseStatus, timeTaken); + apiLogService.saveLog(apiLog); + } + // apilog 생성 private ApiLog buildApiLog(RequestInfoDto info, int responseStatus, long timeTaken) { return ApiLog.builder() @@ -149,7 +95,7 @@ private ApiLog buildApiLog(RequestInfoDto info, int responseStatus, long timeTak .build(); } - private int mapExceptionToStatusCode(Exception e) { + private int mapExceptionToStatusCode(Throwable e) { if (e instanceof GeneralException ge) { return ge.getErrorCode().getCode(); } 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 fa8e0654..be56de74 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,48 +7,76 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import jakarta.annotation.PostConstruct; -import java.io.FileInputStream; -import java.io.FileNotFoundException; +import javax.annotation.PostConstruct; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; @Service @Slf4j public class FirebaseInitialization { - private static final String DEFAULT_FIREBASE_RESOURCE = "ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json"; + @Value("${firebase.credentials.base64:}") + private String firebaseCredentialsBase64; - @Value("${firebase.service-account.path:}") - private String serviceAccountPath; + @Value("${firebase.credentials.json:}") + private String firebaseCredentialsJson; + + @Value("${firebase.credentials.path:}") + private String firebaseCredentialsPath; + + @Value("${google.application.credentials:}") + private String googleApplicationCredentials; @PostConstruct public void initialize() { - try (InputStream serviceAccount = openServiceAccount()) { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(GoogleCredentials.fromStream(serviceAccount)) - .build(); + try { + if (!FirebaseApp.getApps().isEmpty()) { + return; + } + + try (InputStream serviceAccount = resolveCredentials()) { + if (serviceAccount == null) { + log.warn("Firebase credentials were not provided; Firebase push notifications are disabled."); + return; + } + + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .build(); - 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); + log.error("Failed to initialize Firebase.", e); } } - private InputStream openServiceAccount() throws IOException { - if (serviceAccountPath != null && !serviceAccountPath.isBlank()) { - return new FileInputStream(serviceAccountPath); + private InputStream resolveCredentials() throws IOException { + if (hasText(firebaseCredentialsBase64)) { + byte[] decodedCredentials = Base64.getDecoder().decode(firebaseCredentialsBase64); + return new ByteArrayInputStream(decodedCredentials); } - InputStream serviceAccount = getClass().getClassLoader().getResourceAsStream(DEFAULT_FIREBASE_RESOURCE); - if (serviceAccount == null) { - throw new FileNotFoundException("Resource not found: " + DEFAULT_FIREBASE_RESOURCE); + if (hasText(firebaseCredentialsJson)) { + return new ByteArrayInputStream(firebaseCredentialsJson.getBytes(StandardCharsets.UTF_8)); } - return serviceAccount; + + String credentialsPath = hasText(firebaseCredentialsPath) + ? firebaseCredentialsPath + : googleApplicationCredentials; + if (hasText(credentialsPath)) { + return Files.newInputStream(Path.of(credentialsPath)); + } + + return null; + } + + private boolean hasText(String value) { + return value != null && !value.isBlank(); } } diff --git a/ontime-back/src/main/java/devkor/ontime_back/config/SecurityConfig.java b/ontime-back/src/main/java/devkor/ontime_back/config/SecurityConfig.java index 76ea1e4c..43ce7baa 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/config/SecurityConfig.java +++ b/ontime-back/src/main/java/devkor/ontime_back/config/SecurityConfig.java @@ -71,7 +71,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .frameOptions(frameOptions -> frameOptions.disable())) .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/css/**", "/images/**", "/js/**", "/favicon.ico", "/h2-console/**").permitAll() - .requestMatchers("/health", "/oauth2/sign-up", "oauth2/success", "login/success", "/oauth2/google/login", "/oauth2/kakao/login", "/oauth2/apple/login", "/sign-up", "/*/additional-info").permitAll() + .requestMatchers("/health", "/actuator/health/**", "/oauth2/sign-up", "oauth2/success", "login/success", "/oauth2/google/login", "/oauth2/kakao/login", "/oauth2/apple/login", "/sign-up", "/*/additional-info").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/webjars/**", "/swagger-ui.html").permitAll() .requestMatchers("/error").permitAll() .requestMatchers("/health").permitAll() // 로드밸런서 연결 확인용 url diff --git a/ontime-back/src/main/java/devkor/ontime_back/controller/ScheduleController.java b/ontime-back/src/main/java/devkor/ontime_back/controller/ScheduleController.java index 6cc271a6..6c3c7fc8 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/controller/ScheduleController.java +++ b/ontime-back/src/main/java/devkor/ontime_back/controller/ScheduleController.java @@ -282,7 +282,7 @@ public ResponseEntity>> getPreparation(Http content = @Content( schema = @Schema( type = "object", - example = "{\"scheduleId\": \"3fa85f64-5717-4562-b3fc-2c963f66afe5\", \"latenessTime\": 3}" + example = "{\"latenessTime\": 3}" ) ) ) @@ -299,10 +299,11 @@ public ResponseEntity>> getPreparation(Http @PutMapping("/{scheduleId}/finish") // 약속 준비 종료 이후 지각시간(Schedule 테이블), 성실도 점수(User 테이블) 업데이트 public ResponseEntity> finishSchedule( HttpServletRequest request, + @PathVariable UUID scheduleId, @RequestBody FinishPreparationDto finishPreparationDto) { Long userId = userAuthService.getUserIdFromToken(request); - scheduleService.finishSchedule(userId, finishPreparationDto); + scheduleService.finishSchedule(userId, scheduleId, finishPreparationDto); String message = "지각시간과 성실도점수가 성공적으로 업데이트 되었습니다!"; return ResponseEntity.ok(ApiResponseForm.success(null, message)); } diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmDeviceCurrentRequestDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmDeviceCurrentRequestDto.java index 4d1884bc..22604f0e 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmDeviceCurrentRequestDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmDeviceCurrentRequestDto.java @@ -4,11 +4,9 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.ToString; @Getter @Builder -@ToString @NoArgsConstructor @AllArgsConstructor public class AlarmDeviceCurrentRequestDto { diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmDeviceCurrentResponseDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmDeviceCurrentResponseDto.java index c5f1d953..25aaf716 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmDeviceCurrentResponseDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmDeviceCurrentResponseDto.java @@ -3,7 +3,6 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; - import java.time.Instant; @Getter diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmDeviceUnregisterRequestDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmDeviceUnregisterRequestDto.java index 90530f8a..49bdb879 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmDeviceUnregisterRequestDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmDeviceUnregisterRequestDto.java @@ -4,11 +4,9 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.ToString; @Getter @Builder -@ToString @NoArgsConstructor @AllArgsConstructor public class AlarmDeviceUnregisterRequestDto { diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmSettingsResponseDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmSettingsResponseDto.java index 85470545..57d158ac 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmSettingsResponseDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmSettingsResponseDto.java @@ -3,7 +3,6 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; - import java.time.Instant; @Getter diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmStatusCurrentResponseDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmStatusCurrentResponseDto.java index 3274908d..1ab130d7 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmStatusCurrentResponseDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmStatusCurrentResponseDto.java @@ -3,7 +3,6 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; - import java.time.Instant; import java.time.LocalDateTime; import java.util.List; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmStatusFailureDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmStatusFailureDto.java index 86f4055f..1c5fbd66 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmStatusFailureDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmStatusFailureDto.java @@ -4,11 +4,9 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.ToString; @Getter @Builder -@ToString @NoArgsConstructor @AllArgsConstructor public class AlarmStatusFailureDto { diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmStatusReportRequestDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmStatusReportRequestDto.java index a8e78280..bb529921 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmStatusReportRequestDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmStatusReportRequestDto.java @@ -4,15 +4,12 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.ToString; - import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.util.List; @Getter @Builder -@ToString @NoArgsConstructor @AllArgsConstructor public class AlarmStatusReportRequestDto { diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmWindowScheduleDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmWindowScheduleDto.java index f33da759..6dcd968a 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmWindowScheduleDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/AlarmWindowScheduleDto.java @@ -4,7 +4,6 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; - import java.time.LocalDateTime; import java.util.List; import java.util.UUID; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/AppleTokenResponseDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/AppleTokenResponseDto.java index e24fcc0a..8b291b35 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/AppleTokenResponseDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/AppleTokenResponseDto.java @@ -7,17 +7,12 @@ public class AppleTokenResponseDto { @JsonProperty("access_token") private String accessToken; - @JsonProperty("token_type") private String tokenType; - @JsonProperty("expires_in") private long expiresIn; - @JsonProperty("refresh_token") private String refreshToken; - @JsonProperty("id_token") private String idToken; - } diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/ChangePasswordDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/ChangePasswordDto.java index f7fa3eae..92c28bd9 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/ChangePasswordDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/ChangePasswordDto.java @@ -8,7 +8,6 @@ public class ChangePasswordDto { private String currentPassword; private String newPassword; - @Builder public ChangePasswordDto(String currentPassword, String newPassword) { this.currentPassword = currentPassword; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/FeedbackAddDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/FeedbackAddDto.java index b653711a..54c658cb 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/FeedbackAddDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/FeedbackAddDto.java @@ -1,10 +1,12 @@ package devkor.ontime_back.dto; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.UUID; -@ToString @Getter @Builder @NoArgsConstructor diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/FinishPreparationDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/FinishPreparationDto.java index 3623e03a..b42e7815 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/FinishPreparationDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/FinishPreparationDto.java @@ -2,16 +2,12 @@ import lombok.Builder; import lombok.Getter; -import lombok.ToString; - import java.util.UUID; -@ToString @Getter public class FinishPreparationDto { private UUID scheduleId; private Integer latenessTime; - @Builder public FinishPreparationDto(UUID scheduleId, Integer latenessTime) { this.scheduleId = scheduleId; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/FirebaseTokenAddDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/FirebaseTokenAddDto.java index e15f7d6e..c4680868 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/FirebaseTokenAddDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/FirebaseTokenAddDto.java @@ -1,9 +1,7 @@ package devkor.ontime_back.dto; import lombok.Getter; -import lombok.ToString; -@ToString @Getter public class FirebaseTokenAddDto { String firebaseToken; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/FriendDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/FriendDto.java index 8b7c0439..723a6450 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/FriendDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/FriendDto.java @@ -2,16 +2,13 @@ import lombok.Builder; import lombok.Getter; -import lombok.ToString; -@ToString @Getter @Builder public class FriendDto { private Long friendId; private String friendName; private String friendEmail; - public FriendDto(Long friendId, String friendName, String friendEmail) { this.friendId = friendId; this.friendName = friendName; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/GetFriendListResponse.java b/ontime-back/src/main/java/devkor/ontime_back/dto/GetFriendListResponse.java index b092ee3b..2b860e43 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/GetFriendListResponse.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/GetFriendListResponse.java @@ -2,13 +2,10 @@ import lombok.Builder; import lombok.Getter; - import java.util.List; @Getter @Builder public class GetFriendListResponse { - private List friendsList; - } diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/GetFriendshipRequesterResponse.java b/ontime-back/src/main/java/devkor/ontime_back/dto/GetFriendshipRequesterResponse.java index 0118bb03..8997e20c 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/GetFriendshipRequesterResponse.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/GetFriendshipRequesterResponse.java @@ -2,15 +2,12 @@ import lombok.Builder; import lombok.Getter; -import lombok.ToString; -@ToString @Getter public class GetFriendshipRequesterResponse { private Long requesterId; private String requesterName; private String requesterEmail; - @Builder public GetFriendshipRequesterResponse(Long requesterId, String requesterName, String requesterEmail) { this.requesterId = requesterId; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/LatenessHistoryResponse.java b/ontime-back/src/main/java/devkor/ontime_back/dto/LatenessHistoryResponse.java index 85f2039d..40ecc0a4 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/LatenessHistoryResponse.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/LatenessHistoryResponse.java @@ -1,19 +1,15 @@ package devkor.ontime_back.dto; import lombok.Getter; -import lombok.ToString; - import java.time.LocalDateTime; import java.util.UUID; -@ToString @Getter public class LatenessHistoryResponse { private UUID scheduleId; private String scheduleName; private LocalDateTime scheduleTime; private int latenessTime; - public LatenessHistoryResponse(UUID scheduleId, String scheduleName, LocalDateTime scheduleTime, int latenessTime) { this.scheduleId = scheduleId; this.scheduleName = scheduleName; @@ -21,4 +17,3 @@ public LatenessHistoryResponse(UUID scheduleId, String scheduleName, LocalDateTi this.latenessTime = latenessTime; } } - diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/OAuthAppleUserDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/OAuthAppleUserDto.java index 83928368..3ff579fe 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/OAuthAppleUserDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/OAuthAppleUserDto.java @@ -7,7 +7,6 @@ public class OAuthAppleUserDto { private String appleUserId; private String email; private String fullName; - public OAuthAppleUserDto(String appleUserId, String email, String fullName) { this.appleUserId = appleUserId; this.email = email; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/OAuthGoogleUserDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/OAuthGoogleUserDto.java index 9fcd31b8..d835e30b 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/OAuthGoogleUserDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/OAuthGoogleUserDto.java @@ -4,12 +4,10 @@ @Getter public class OAuthGoogleUserDto { - private String id; // 고유 사용자 ID private String name; // 사용자 이름 private String picture; // 프로필 이미지 URL private String email; // 이메일 - public OAuthGoogleUserDto(String id, String name, String picture, String email) { this.id = id; this.name = name; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/OAuthKakaoUserDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/OAuthKakaoUserDto.java index b8d2dc84..e3887343 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/OAuthKakaoUserDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/OAuthKakaoUserDto.java @@ -7,7 +7,6 @@ public class OAuthKakaoUserDto { private String id; private Profile profile; - @Data public static class Profile { private String nickname; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/PlaceDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/PlaceDto.java index 0a2e7319..7eb2068d 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/PlaceDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/PlaceDto.java @@ -3,7 +3,6 @@ import devkor.ontime_back.entity.Place; import lombok.AllArgsConstructor; import lombok.Getter; - import java.util.UUID; @Getter @@ -11,7 +10,6 @@ public class PlaceDto { private UUID placeId; private String placeName; - public static PlaceDto fromEntity(Place place) { return new PlaceDto(place.getPlaceId(), place.getPlaceName()); } diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/PreparationDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/PreparationDto.java index 4fa774e2..34676a05 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/PreparationDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/PreparationDto.java @@ -3,8 +3,6 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; - -import java.sql.Time; import java.util.UUID; @AllArgsConstructor @@ -12,20 +10,7 @@ @Builder public class PreparationDto { private UUID preparationId; - private String preparationName; - private Integer preparationTime; - private UUID nextPreparationId; - - @Override - public String toString() { - return "PreparationDto{" + - "preparationId=" + preparationId + - ", preparationName='" + preparationName + '\'' + - ", preparationTime=" + preparationTime + - ", nextPreparationId=" + nextPreparationId + - '}'; - } } diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/PunctualityScoreResponse.java b/ontime-back/src/main/java/devkor/ontime_back/dto/PunctualityScoreResponse.java index bbf69e57..315c547f 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/PunctualityScoreResponse.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/PunctualityScoreResponse.java @@ -2,9 +2,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.ToString; -@ToString @AllArgsConstructor @Getter public class PunctualityScoreResponse { diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/RequestInfoDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/RequestInfoDto.java index 6d03ccb6..495feb04 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/RequestInfoDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/RequestInfoDto.java @@ -1,6 +1,5 @@ package devkor.ontime_back.dto; - import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/ScheduleAddDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/ScheduleAddDto.java index 8f7458de..381fcb80 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/ScheduleAddDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/ScheduleAddDto.java @@ -7,12 +7,9 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.ToString; - import java.time.LocalDateTime; import java.util.UUID; -@ToString @Getter @Builder @AllArgsConstructor @@ -27,7 +24,6 @@ public class ScheduleAddDto { private Boolean isStarted; // 버튼누름여부 private Integer scheduleSpareTime; // 스케줄 별 여유시간 private String scheduleNote; // 스케줄 별 주의사항 - public Schedule toEntity(User user, Place place) { return Schedule.builder() .user(user) diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/ScheduleDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/ScheduleDto.java index 118125a9..260f71e1 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/ScheduleDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/ScheduleDto.java @@ -5,12 +5,10 @@ import devkor.ontime_back.entity.User; import jakarta.persistence.*; import lombok.*; - import java.sql.Time; import java.time.LocalDateTime; import java.util.UUID; -@ToString @Data @NoArgsConstructor // 기본 생성자 추가 @AllArgsConstructor // 모든 필드를 포함하는 생성자 추가 @@ -24,5 +22,4 @@ public class ScheduleDto { private String scheduleNote; private Integer latenessTime; private DoneStatus doneStatus; - } diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/ScheduleModDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/ScheduleModDto.java index c5493508..02438260 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/ScheduleModDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/ScheduleModDto.java @@ -3,31 +3,19 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.ToString; - import java.time.LocalDateTime; import java.util.UUID; -@ToString @Getter @Builder @AllArgsConstructor public class ScheduleModDto { - private UUID placeId; - private String placeName; - private String scheduleName; - private Integer moveTime; // 이동시간 - private LocalDateTime scheduleTime; // 약속시각 - private Integer scheduleSpareTime; // 스케줄 별 여유시간 - private Integer latenessTime; - private String scheduleNote; // 스케줄 별 주의사항 - } diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/SchedulePeriodDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/SchedulePeriodDto.java index 48179fa8..81f4677f 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/SchedulePeriodDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/SchedulePeriodDto.java @@ -1,11 +1,8 @@ package devkor.ontime_back.dto; import lombok.Getter; -import lombok.ToString; - import java.time.LocalDateTime; -@ToString @Getter public class SchedulePeriodDto { LocalDateTime startDate; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/UpdateAcceptStatusDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/UpdateAcceptStatusDto.java index ad9b527b..c19dbe22 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/UpdateAcceptStatusDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/UpdateAcceptStatusDto.java @@ -1,9 +1,7 @@ package devkor.ontime_back.dto; import lombok.Getter; -import lombok.ToString; -@ToString @Getter public class UpdateAcceptStatusDto { private String acceptStatus; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/UpdatePunctualityScoreDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/UpdatePunctualityScoreDto.java index 0b90cfed..a7325ef4 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/UpdatePunctualityScoreDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/UpdatePunctualityScoreDto.java @@ -1,9 +1,7 @@ package devkor.ontime_back.dto; import lombok.Getter; -import lombok.ToString; -@ToString @Getter public class UpdatePunctualityScoreDto { private Long userId; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/UpdateSpareTimeDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/UpdateSpareTimeDto.java index 9b364dce..792e0676 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/UpdateSpareTimeDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/UpdateSpareTimeDto.java @@ -3,14 +3,11 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.ToString; -@ToString @Getter @NoArgsConstructor public class UpdateSpareTimeDto { private Integer newSpareTime; - @Builder public UpdateSpareTimeDto(Integer newSpareTime) { this.newSpareTime = newSpareTime; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/UserAdditionalInfoDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/UserAdditionalInfoDto.java index b705151d..8a6ac549 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/UserAdditionalInfoDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/UserAdditionalInfoDto.java @@ -1,15 +1,12 @@ package devkor.ontime_back.dto; import lombok.*; - import java.sql.Time; -@ToString @Getter public class UserAdditionalInfoDto { private Integer spareTime; // 여유시간 private String note; // 주의사항 - @Builder public UserAdditionalInfoDto(Integer spareTime, String note) { this.spareTime = spareTime; diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/UserInfoResponse.java b/ontime-back/src/main/java/devkor/ontime_back/dto/UserInfoResponse.java index c671f72d..c75d50c8 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/UserInfoResponse.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/UserInfoResponse.java @@ -5,9 +5,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.ToString; -@ToString @AllArgsConstructor @Getter @Builder diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/UserOnboardingDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/UserOnboardingDto.java index 57a167a5..5f8e7e70 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/UserOnboardingDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/UserOnboardingDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; - import java.util.List; @Getter @@ -11,5 +10,4 @@ public class UserOnboardingDto { private Integer spareTime; private String note; private List preparationList; - } diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/UserSettingUpdateDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/UserSettingUpdateDto.java index 83666e12..1b87922b 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/UserSettingUpdateDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/UserSettingUpdateDto.java @@ -2,9 +2,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.ToString; -@ToString @Getter @NoArgsConstructor public class UserSettingUpdateDto { diff --git a/ontime-back/src/main/java/devkor/ontime_back/dto/UserSignUpDto.java b/ontime-back/src/main/java/devkor/ontime_back/dto/UserSignUpDto.java index 55a364a5..27ced8e0 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/dto/UserSignUpDto.java +++ b/ontime-back/src/main/java/devkor/ontime_back/dto/UserSignUpDto.java @@ -4,7 +4,6 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; - import java.util.UUID; @NoArgsConstructor 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 fc5f18a1..c8e523f7 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.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; @@ -21,9 +20,6 @@ public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JwtTokenProvider jwtTokenProvider; private final UserRepository userRepository; - @Value("${jwt.access.expiration}") - private String accessTokenExpiration; - @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { @@ -42,8 +38,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo user.updateRefreshToken(refreshToken); userRepository.saveAndFlush(user); - log.info("로그인에 성공하였습니다. 이메일 : {}", email); - log.info("발급된 AccessToken 만료 기간 : {}", accessTokenExpiration); + log.info("Login succeeded for userId: {}", user.getId()); try { diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilter.java b/ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilter.java index 25bc5616..fedf244b 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilter.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilter.java @@ -32,7 +32,7 @@ @Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter { - private static final List NO_CHECK_URLS = List.of("/login", "/swagger-ui", "/sign-up", "/v3/api-docs", "/oauth2/google/login", "/oauth2/kakao/login", "/oauth2/apple/login"); + private static final List NO_CHECK_URLS = List.of("/login", "/health", "/actuator/health", "/swagger-ui", "/sign-up", "/v3/api-docs", "/oauth2/google/login", "/oauth2/kakao/login", "/oauth2/apple/login"); private final JwtTokenProvider jwtTokenProvider; private final UserRepository userRepository; @@ -60,7 +60,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse // 리프레시토큰의 경우 토큰의 유효성 뿐만 아니라 DB에 등록되어 있는지도 확인해야 함 // reIssueAccessToken 메소드에서 DB를 확인해 등록된 리프레시 토큰이면 엑세스 토큰 재발급 // 이때 reIssueAccessToken 메소드에서 DB에 등록된 리프레시 토큰이 아니면 InvalidRefreshTokenException 발생 - log.info("리프레시 토큰이 있고 유효한데 DB에 있는지는 아직 모름"); + log.info("Refresh credential passed signature validation; checking stored credential"); reIssueAccessToken(response, refreshToken); return; } @@ -90,10 +90,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse // 리프레시 토큰이 DB에 있으면 엑세스 토큰을 재발급 // DB에 없으면 InvalidRefreshTokenException 발생 public void reIssueAccessToken(HttpServletResponse response, String refreshToken) throws IOException { - log.info("리프레시토큰이 유효하나 DB에 있는지는 모름. DB에서 찾아봐서 없으면 예외 발생할 것임."); + log.info("Checking stored refresh credential"); User user = userRepository.findByRefreshToken(refreshToken) .orElseThrow(() -> new InvalidRefreshTokenException("Invalid Refresh token!~!")); - log.info("리프레시토큰이 DB에도 있음"); + log.info("Stored refresh credential matched"); String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getId()); @@ -105,11 +105,11 @@ public void reIssueAccessToken(HttpServletResponse response, String refreshToken // accessToken으로 유저의 권한정보만 저장하고 인증 허가(스프링 시큐리티 필터체인 中 인증체인 통과해 다음 체인으로 이동) public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - log.info("checkAccessTokenAndAuthentication() 호출"); + log.info("Checking access credential authentication"); jwtTokenProvider.extractAccessToken(request) .ifPresent(accessToken -> jwtTokenProvider.extractUserId(accessToken) .ifPresent(userId -> { - log.info("추출된 userId: {}", userId); + log.info("Authenticated userId: {}", userId); userRepository.findById(userId) .ifPresent(this::saveAuthentication); })); @@ -155,7 +155,7 @@ private void handleInvalidTokenException(HttpServletResponse response, InvalidTo response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - log.info("InvalidTokenException 발생"); + log.info("Credential validation exception occurred"); // ErrorCode에서 정보를 가져옴 ErrorCode errorCode = ErrorCode.UNAUTHORIZED; 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 8c7f5191..2c27b8ca 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 @@ -55,7 +55,6 @@ public class JwtTokenProvider { // accessToken 생성 public String createAccessToken(String email, Long userId) { Date now = new Date(); - log.info("expiresAt: {}", new Date(now.getTime() + accessTokenExpirationPeriod)); return JWT.create() .withSubject(ACCESS_TOKEN_SUBJECT) .withExpiresAt(new Date(now.getTime() + accessTokenExpirationPeriod)) @@ -80,7 +79,6 @@ public void sendAccessToken(HttpServletResponse response, String accessToken) { response.setStatus(HttpServletResponse.SC_OK); response.setHeader(accessHeader, accessToken); - log.info("Access Token 헤더 설정 완료"); } // accessToken + refreshToken header에 넣어서 전송 @@ -89,7 +87,7 @@ public void sendAccessAndRefreshToken(HttpServletResponse response, String acces setAccessTokenHeader(response, accessToken); setRefreshTokenHeader(response, refreshToken); - log.info("Access Token, Refresh Token 헤더 설정 완료"); + log.info("Credential headers set"); } // header에서 refreshToken 추출 @@ -115,7 +113,7 @@ public Optional extractEmail(String accessToken) { .getClaim(EMAIL_CLAIM) .asString()); } catch (Exception e) { - log.error("액세스 토큰이 유효하지 않습니다.as"); + log.error("Access credential is invalid"); return Optional.empty(); } } @@ -129,7 +127,7 @@ public Optional extractUserId(String accessToken) { .getClaim(USER_ID_CLAIM) .asLong()); } catch (Exception e) { - log.error("유효하지 않은 accessToken입니다."); + log.error("Access credential is invalid"); return Optional.empty(); } } @@ -158,10 +156,10 @@ public void updateRefreshToken(String email, String refreshToken) { public boolean isTokenValid(String token) { try { JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token); - log.info("유효한 토큰입니다."); + log.info("Credential is valid"); return true; } catch (Exception e) { - log.error("유효하지 않은 토큰입니다. {}", e.getMessage()); + log.error("Credential is invalid"); throw new InvalidTokenException("유효하지 않은 토큰입니다."); } } @@ -171,10 +169,10 @@ public boolean isAccessTokenValid(String token) { userRepository.findByAccessToken(token) .orElseThrow(() -> new InvalidAccessTokenException("유효하지 않은 엑세스 토큰입니다.")); JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token); - log.info("유효한 엑세스 토큰입니다."); + log.info("Access credential is valid"); return true; } catch (Exception e) { - log.error("유효하지 않은 엑세스 토큰입니다. {}", e.getMessage()); + log.error("Access credential is invalid"); throw new InvalidAccessTokenException("유효하지 않은 엑세스 토큰입니다."); } } @@ -182,10 +180,10 @@ public boolean isAccessTokenValid(String token) { public boolean isRefreshTokenValid(String token) { try { JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token); - log.info("유효한 리프레시 토큰입니다."); + log.info("Refresh credential is valid"); return true; } catch (Exception e) { - log.error("유효하지 않은 리프레시 토큰입니다. {}", e.getMessage()); + log.error("Refresh credential is invalid"); throw new InvalidRefreshTokenException("유효하지 않은 리프레시 토큰입니다."); } } diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginFilter.java b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginFilter.java index c28ec3dc..fc2922b9 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginFilter.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginFilter.java @@ -69,7 +69,6 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ } String appleUserId = tokenClaims.getSubject(); - log.info("appleUserId: {}", appleUserId); String email = tokenClaims.get("email", String.class); // socialRefreshtoken에 저장 @@ -86,10 +85,9 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ } } catch (Exception e) { - log.error("Apple 로그인 실패: {}", e.getMessage(), e); + log.error("Apple login failed: {}", e.getClass().getSimpleName()); throw new AuthenticationException("Apple 로그인 실패") {}; } } } - 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 d91fc028..41c4d593 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 @@ -34,6 +34,7 @@ import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyFactory; @@ -58,8 +59,12 @@ public class AppleLoginService { private String teamId; @Value("${apple.login.key}") private String keyId; - @Value("${apple.client.secret}") + @Value("${apple.client.secret:}") private String privateKeyPath; + @Value("${apple.private-key.base64:}") + private String privateKeyBase64; + @Value("${apple.private-key:}") + private String privateKey; private final ApplePublicKeyGenerator applePublicKeyGenerator; private final JwtUtils jwtUtils; @@ -164,7 +169,7 @@ public Authentication handleRegister(String appleRefreshToken, OAuthAppleUserDto // identitytoken 검증 public Claims verifyIdentityToken(String identityToken) throws Exception { - log.info("verifyIdentityToken"); + log.info("Verify Apple identity credential"); Map headers = jwtUtils.parseHeaders(identityToken); // apple publickey ApplePublicKeyResponse applePublicKeyResponse = restTemplate.getForObject(APPLE_KEYS_URL, ApplePublicKeyResponse.class); @@ -192,8 +197,7 @@ public Claims verifyIdentityToken(String identityToken) throws public AppleTokenResponseDto getAppleAccessTokenAndRefreshToken(String authCode) throws Exception { // clientSecret String clientSecret = generateClientSecret(); - log.info("getAppleAccessTokenAndRefreshToken"); - log.info("client_id: {}", clientId); + log.info("Exchange Apple credential"); MultiValueMap requestBody = new LinkedMultiValueMap<>(); requestBody.add("grant_type", "authorization_code"); requestBody.add("code", authCode); @@ -217,9 +221,9 @@ public AppleTokenResponseDto getAppleAccessTokenAndRefreshToken(String authCode) // clientsecret 생성 private String generateClientSecret() throws Exception { - log.info("generageClientSecret"); + log.info("Generate Apple client credential"); // Private Key - String privateKeyContent = new String(Files.readAllBytes(Paths.get(privateKeyPath))) + String privateKeyContent = resolvePrivateKey() .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s", ""); @@ -244,6 +248,20 @@ private String generateClientSecret() throws Exception { .signWith(privateKey, SignatureAlgorithm.ES256) .compact(); } + + private String resolvePrivateKey() throws IOException { + if (privateKeyBase64 != null && !privateKeyBase64.isBlank()) { + byte[] decodedPrivateKey = Base64.getDecoder().decode(privateKeyBase64); + return new String(decodedPrivateKey, StandardCharsets.UTF_8); + } + + if (privateKey != null && !privateKey.isBlank()) { + return privateKey; + } + + return new String(Files.readAllBytes(Paths.get(privateKeyPath))); + } + public boolean revokeToken(Long userId) throws Exception { log.info("checkAppleLoginRevoked"); User user = userRepository.findById(userId) 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 0c237eac..45ab78a8 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 @@ -73,7 +73,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ } } catch (Exception e) { - log.error("Google 로그인 실패: {}", e.getMessage(), e); + log.error("Google login failed: {}", e.getClass().getSimpleName()); throw new AuthenticationException("Google 로그인 실패") {}; } 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 a2379e32..3b64d38c 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 @@ -165,7 +165,7 @@ public GoogleIdToken.Payload verifyIdentityToken(String identityToken) throws Ex GoogleIdToken.Payload payload = idToken.getPayload(); return payload; } else { - log.info("유효하지 않은 idtoken 입니다."); + log.info("Google identity credential is invalid"); return null; } } diff --git a/ontime-back/src/main/java/devkor/ontime_back/logging/RequestLogPolicy.java b/ontime-back/src/main/java/devkor/ontime_back/logging/RequestLogPolicy.java new file mode 100644 index 00000000..3927da9c --- /dev/null +++ b/ontime-back/src/main/java/devkor/ontime_back/logging/RequestLogPolicy.java @@ -0,0 +1,81 @@ +package devkor.ontime_back.logging; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; + +public final class RequestLogPolicy { + + public static final String REQUEST_ID_HEADER = "X-Request-Id"; + public static final String REQUEST_ID_ATTRIBUTE = RequestLogPolicy.class.getName() + ".requestId"; + + private static final Pattern SAFE_REQUEST_ID = Pattern.compile("[A-Za-z0-9._:-]{1,128}"); + + private static final Set SENSITIVE_FIELD_NAMES = Set.of( + "authorization", + "authCode", + "clientSecret", + "client_secret", + "currentPassword", + "firebaseToken", + "idToken", + "newPassword", + "password", + "refreshToken", + "secret", + "token" + ); + + private static final Set SAFE_FIELD_ALLOWLIST = Set.of( + "appVersion", + "armedScheduleCount", + "clientIp", + "deviceId", + "method", + "osVersion", + "platform", + "requestId", + "responseStatus", + "route", + "timeTakenMs", + "userId" + ); + + private RequestLogPolicy() { + } + + public static String resolveRequestId(HttpServletRequest request) { + Object existingRequestId = request.getAttribute(REQUEST_ID_ATTRIBUTE); + if (existingRequestId instanceof String existing && !existing.isBlank()) { + return existing; + } + + String requestId = request.getHeader(REQUEST_ID_HEADER); + if (requestId == null || !SAFE_REQUEST_ID.matcher(requestId).matches()) { + requestId = UUID.randomUUID().toString(); + } + + request.setAttribute(REQUEST_ID_ATTRIBUTE, requestId); + return requestId; + } + + public static void exposeRequestId(ServletRequestAttributes attributes, String requestId) { + HttpServletResponse response = attributes.getResponse(); + if (response != null) { + response.setHeader(REQUEST_ID_HEADER, requestId); + } + } + + public static boolean isSafeFieldForLogging(String fieldName) { + return SAFE_FIELD_ALLOWLIST.contains(fieldName) && !isSensitiveFieldName(fieldName); + } + + public static boolean isSensitiveFieldName(String fieldName) { + return SENSITIVE_FIELD_NAMES.stream() + .anyMatch(sensitiveField -> sensitiveField.equalsIgnoreCase(fieldName)); + } +} diff --git a/ontime-back/src/main/java/devkor/ontime_back/response/ErrorCode.java b/ontime-back/src/main/java/devkor/ontime_back/response/ErrorCode.java index 3117cab2..366b15da 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/response/ErrorCode.java +++ b/ontime-back/src/main/java/devkor/ontime_back/response/ErrorCode.java @@ -30,6 +30,8 @@ public enum ErrorCode { FIRST_PREPARATION_NOT_FOUND(1012, "해당 ID의 사용자의 준비과정을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST), NOTIFICATION_NOT_FOUND(1013, "알림을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST ), PREPARATION_ALREADY_EXISTS(1014, "해당 사용자의 준비과정이 이미 존재합니다.", HttpStatus.BAD_REQUEST), + SCHEDULE_ALREADY_FINISHED(1015, "이미 종료된 약속입니다.", HttpStatus.BAD_REQUEST), + SCHEDULE_ID_MISMATCH(1016, "경로의 scheduleId와 요청 본문의 scheduleId가 일치하지 않습니다.", HttpStatus.BAD_REQUEST), ALARM_SETTINGS_INVALID_FIELD(1101, "ALARM_SETTINGS_INVALID_FIELD", HttpStatus.BAD_REQUEST), ALARM_WINDOW_RANGE_TOO_LONG(1102, "ALARM_WINDOW_RANGE_TOO_LONG", HttpStatus.BAD_REQUEST), DEVICE_SESSION_NOT_ACTIVE(1103, "DEVICE_SESSION_NOT_ACTIVE", HttpStatus.CONFLICT), diff --git a/ontime-back/src/main/java/devkor/ontime_back/response/GlobalExceptionHandler.java b/ontime-back/src/main/java/devkor/ontime_back/response/GlobalExceptionHandler.java index d0d8f1b0..b8c21592 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/response/GlobalExceptionHandler.java +++ b/ontime-back/src/main/java/devkor/ontime_back/response/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package devkor.ontime_back.response; +import devkor.ontime_back.logging.RequestLogPolicy; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -30,11 +31,14 @@ public ResponseEntity> handleInvalidTokenException(Invalid @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex, HttpServletRequest request) { - log.error("[Error Log] requestUrl: {}, requestMethod: {}, userId: {}, clientIp: {}, exception: {}, message: {}, responseStatus: {}", - request.getRequestURI(), request.getMethod(), (request.getUserPrincipal() != null) ? request.getUserPrincipal().getName() : "Anonymous", request.getRemoteAddr(), "HttpMessageNotReadableException", "요청 형식이 올바르지 않습니다.", 400); + String requestId = RequestLogPolicy.resolveRequestId(request); + + log.error("[Error Log] requestId: {}, route: {}, method: {}, actor: {}, clientIp: {}, exception: {}, responseStatus: {}", + requestId, request.getRequestURI(), request.getMethod(), (request.getUserPrincipal() != null) ? request.getUserPrincipal().getName() : "Anonymous", request.getRemoteAddr(), "HttpMessageNotReadableException", 400); return ResponseEntity .status(HttpStatus.BAD_REQUEST) + .header(RequestLogPolicy.REQUEST_ID_HEADER, requestId) .body(ApiResponseForm.error(400, "요청 형식이 올바르지 않습니다.")); } -} \ No newline at end of file +} diff --git a/ontime-back/src/main/java/devkor/ontime_back/service/ScheduleService.java b/ontime-back/src/main/java/devkor/ontime_back/service/ScheduleService.java index 172a9976..5cfca01d 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/service/ScheduleService.java +++ b/ontime-back/src/main/java/devkor/ontime_back/service/ScheduleService.java @@ -173,20 +173,28 @@ public List getLatenessHistory(Long userId) { // 지각 시간 업데이트 @Transactional - public void updateLatenessTime(FinishPreparationDto finishPreparationDto) { - UUID scheduleId = finishPreparationDto.getScheduleId(); - Integer latenessTime = finishPreparationDto.getLatenessTime(); - - Schedule schedule = scheduleRepository.findById(scheduleId) - .orElseThrow(() -> new GeneralException(SCHEDULE_NOT_FOUND)); - + public void updateLatenessTime(Schedule schedule, Integer latenessTime) { schedule.updateLatenessTime(latenessTime); scheduleRepository.save(schedule); } @Transactional - public void finishSchedule(Long userId, FinishPreparationDto finishPreparationDto) { - updateLatenessTime(finishPreparationDto); + public void finishSchedule(Long userId, UUID scheduleId, FinishPreparationDto finishPreparationDto) { + if (finishPreparationDto == null || finishPreparationDto.getLatenessTime() == null) { + throw new GeneralException(INVALID_INPUT); + } + if (finishPreparationDto.getScheduleId() != null && !scheduleId.equals(finishPreparationDto.getScheduleId())) { + throw new GeneralException(SCHEDULE_ID_MISMATCH); + } + + Schedule schedule = getScheduleWithAuthorization(scheduleId, userId); + boolean alreadyFinishedByDoneStatus = schedule.getDoneStatus() != null && schedule.getDoneStatus() != DoneStatus.NOT_ENDED; + boolean alreadyFinishedByLatenessTime = schedule.getLatenessTime() != null && schedule.getLatenessTime() != -1; + if (alreadyFinishedByDoneStatus || alreadyFinishedByLatenessTime) { + throw new GeneralException(SCHEDULE_ALREADY_FINISHED); + } + + updateLatenessTime(schedule, finishPreparationDto.getLatenessTime()); userService.updatePunctualityScore(userId, finishPreparationDto.getLatenessTime()); } diff --git a/ontime-back/src/main/resources/application-prod.properties b/ontime-back/src/main/resources/application-prod.properties new file mode 100644 index 00000000..851a6410 --- /dev/null +++ b/ontime-back/src/main/resources/application-prod.properties @@ -0,0 +1,7 @@ +management.endpoint.health.probes.enabled=true +management.endpoints.web.exposure.include=health +management.health.readinessstate.enabled=true +management.health.livenessstate.enabled=true +server.shutdown=graceful +spring.lifecycle.timeout-per-shutdown-phase=30s +server.forward-headers-strategy=framework diff --git a/ontime-back/src/test/java/devkor/ontime_back/controller/ScheduleControllerTest.java b/ontime-back/src/test/java/devkor/ontime_back/controller/ScheduleControllerTest.java index 3395de68..47f55717 100644 --- a/ontime-back/src/test/java/devkor/ontime_back/controller/ScheduleControllerTest.java +++ b/ontime-back/src/test/java/devkor/ontime_back/controller/ScheduleControllerTest.java @@ -7,6 +7,8 @@ import devkor.ontime_back.TestSecurityConfig; import devkor.ontime_back.dto.*; import devkor.ontime_back.entity.DoneStatus; +import devkor.ontime_back.response.ErrorCode; +import devkor.ontime_back.response.GeneralException; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -261,16 +263,18 @@ void getLatenessHistory() throws Exception { @Test void finishSchedule() throws Exception { // given + Long userId = 1L; + UUID scheduleId = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"); FinishPreparationDto finishPreparationDto = FinishPreparationDto.builder() - .scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) + .latenessTime(0) .build(); - when(userAuthService.getUserIdFromToken(any())).thenReturn(1L); - doNothing().when(scheduleService).finishSchedule(any(Long.class), any(FinishPreparationDto.class)); + when(userAuthService.getUserIdFromToken(any())).thenReturn(userId); + doNothing().when(scheduleService).finishSchedule(eq(userId), eq(scheduleId), any(FinishPreparationDto.class)); // when // then mockMvc.perform( - put("/schedules/" + 1L + "/finish") + put("/schedules/{scheduleId}/finish", scheduleId) .content(objectMapper.writeValueAsString(finishPreparationDto)) .contentType(MediaType.APPLICATION_JSON) ) @@ -279,5 +283,35 @@ void finishSchedule() throws Exception { .andExpect(jsonPath("$.code").value("200")) .andExpect(jsonPath("$.status").value("success")) .andExpect(jsonPath("$.message").value("지각시간과 성실도점수가 성공적으로 업데이트 되었습니다!")); + + verify(scheduleService, times(1)).finishSchedule(eq(userId), eq(scheduleId), any(FinishPreparationDto.class)); + } + + @DisplayName("약속 종료 요청에서 경로와 본문의 scheduleId가 다르면 400을 반환한다.") + @Test + void finishSchedule_failByScheduleIdMismatch() throws Exception { + // given + Long userId = 1L; + UUID pathScheduleId = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"); + FinishPreparationDto finishPreparationDto = FinishPreparationDto.builder() + .scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe6")) + .latenessTime(0) + .build(); + + when(userAuthService.getUserIdFromToken(any())).thenReturn(userId); + doThrow(new GeneralException(ErrorCode.SCHEDULE_ID_MISMATCH)) + .when(scheduleService).finishSchedule(eq(userId), eq(pathScheduleId), any(FinishPreparationDto.class)); + + // when // then + mockMvc.perform( + put("/schedules/{scheduleId}/finish", pathScheduleId) + .content(objectMapper.writeValueAsString(finishPreparationDto)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.SCHEDULE_ID_MISMATCH.getCode())) + .andExpect(jsonPath("$.status").value("error")) + .andExpect(jsonPath("$.message").value(ErrorCode.SCHEDULE_ID_MISMATCH.getMessage())); } -} \ No newline at end of file +} diff --git a/ontime-back/src/test/java/devkor/ontime_back/security/SensitiveLoggingPolicyTest.java b/ontime-back/src/test/java/devkor/ontime_back/security/SensitiveLoggingPolicyTest.java new file mode 100644 index 00000000..f0260b0f --- /dev/null +++ b/ontime-back/src/test/java/devkor/ontime_back/security/SensitiveLoggingPolicyTest.java @@ -0,0 +1,97 @@ +package devkor.ontime_back.security; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class SensitiveLoggingPolicyTest { + + private static final Path MAIN_SOURCE_ROOT = Path.of("src/main/java"); + private static final Pattern LOG_STATEMENT = Pattern.compile( + "log\\.(?:trace|debug|info|warn|error)\\s*\\((.*?)\\);", + Pattern.DOTALL + ); + private static final List SENSITIVE_LOG_TERMS = List.of( + "authorization", + "firebaseToken", + "password", + "secret", + "token" + ); + + @Test + void logStatementsDoNotReferenceSensitiveKeyNames() throws IOException { + try (Stream sourceFiles = Files.walk(MAIN_SOURCE_ROOT)) { + List violations = sourceFiles + .filter(path -> path.toString().endsWith(".java")) + .flatMap(this::findSensitiveLogStatements) + .toList(); + + assertThat(violations) + .as("Log statements must not reference sensitive key names or sensitive variables") + .isEmpty(); + } + } + + @Test + void requestLoggingAspectDoesNotReadRequestBodies() throws IOException { + String loggingAspect = Files.readString(MAIN_SOURCE_ROOT.resolve("devkor/ontime_back/LoggingAspect.java")); + + assertThat(loggingAspect) + .doesNotContain("org.springframework.web.bind.annotation.RequestBody") + .doesNotContain("requestBody") + .doesNotContain(".toString()"); + } + + @Test + void dtoPackageDoesNotGenerateStringRepresentations() throws IOException { + try (Stream dtoFiles = Files.walk(MAIN_SOURCE_ROOT.resolve("devkor/ontime_back/dto"))) { + List violations = dtoFiles + .filter(path -> path.toString().endsWith(".java")) + .filter(path -> { + try { + String source = Files.readString(path); + return source.contains("@ToString") || source.contains("String toString()"); + } catch (IOException e) { + throw new IllegalStateException(e); + } + }) + .map(Path::toString) + .toList(); + + assertThat(violations) + .as("DTOs should not auto-render sensitive request payloads") + .isEmpty(); + } + } + + private Stream findSensitiveLogStatements(Path sourceFile) { + try { + String source = Files.readString(sourceFile); + Matcher matcher = LOG_STATEMENT.matcher(source); + Stream.Builder violations = Stream.builder(); + + while (matcher.find()) { + String statement = matcher.group(1); + String normalizedStatement = statement.toLowerCase(Locale.ROOT); + SENSITIVE_LOG_TERMS.stream() + .filter(term -> normalizedStatement.contains(term.toLowerCase(Locale.ROOT))) + .findFirst() + .ifPresent(term -> violations.add(sourceFile + " logs sensitive term: " + term)); + } + + return violations.build(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/ontime-back/src/test/java/devkor/ontime_back/service/ScheduleServiceTest.java b/ontime-back/src/test/java/devkor/ontime_back/service/ScheduleServiceTest.java index 9b1887ef..9f996d7b 100644 --- a/ontime-back/src/test/java/devkor/ontime_back/service/ScheduleServiceTest.java +++ b/ontime-back/src/test/java/devkor/ontime_back/service/ScheduleServiceTest.java @@ -1233,16 +1233,17 @@ void updateLatenessTime(){ .build(); // when - scheduleService.updateLatenessTime(finishPreparationDto); + Schedule schedule = scheduleRepository.findById(finishPreparationDto.getScheduleId()).get(); + scheduleService.updateLatenessTime(schedule, finishPreparationDto.getLatenessTime()); // then assertThat(scheduleRepository.findById(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) .get().getLatenessTime()).isEqualTo(1); } - @DisplayName("지각 시간 업데이트할 때, 잘못된 schedulId를 DTO에 담아 요청하는 경우 예외가 발생한다.") + @DisplayName("약속 종료할 때, 경로와 본문의 scheduleId가 다르면 예외가 발생한다.") @Test - void updateLatenessTimeWithWrongScheduleId(){ + void finishScheduleWithScheduleIdMismatch(){ // given User addedUser = User.builder() .email("user@example.com") @@ -1269,9 +1270,19 @@ void updateLatenessTimeWithWrongScheduleId(){ .build(); // when // then - assertThatThrownBy(() -> scheduleService.updateLatenessTime(finishPreparationDto)) + assertThatThrownBy(() -> scheduleService.finishSchedule( + addedUser.getId(), + UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"), + finishPreparationDto + )) .isInstanceOf(GeneralException.class) - .hasMessage("해당 약속이 존재하지 않습니다."); + .hasMessage(ErrorCode.SCHEDULE_ID_MISMATCH.getMessage()) + .extracting("errorCode") + .isEqualTo(ErrorCode.SCHEDULE_ID_MISMATCH); + + assertThat(scheduleRepository.findById(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) + .get().getLatenessTime()).isEqualTo(-1); + assertThat(userRepository.findById(addedUser.getId()).get().getPunctualityScore()).isEqualTo(-1f); } @DisplayName("약속을 종료해 지각시간과 성실도점수 업데이트에 성공한다.") @@ -1298,12 +1309,11 @@ void finishSchedule(){ scheduleRepository.save(addedSchedule); FinishPreparationDto finishPreparationDto = FinishPreparationDto.builder() - .scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) .latenessTime(1) .build(); // when - scheduleService.finishSchedule(addedUser.getId(), finishPreparationDto); + scheduleService.finishSchedule(addedUser.getId(), addedSchedule.getScheduleId(), finishPreparationDto); // then assertThat(scheduleRepository.findById(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) @@ -1325,6 +1335,16 @@ void finishScheduleWithWrongUserId(){ .build(); userRepository.save(addedUser); + User otherUser = User.builder() + .email("other@example.com") + .password(passwordEncoder.encode("password1234")) + .name("other") + .punctualityScore(-1f) + .scheduleCountAfterReset(0) + .latenessCountAfterReset(0) + .build(); + userRepository.save(otherUser); + Schedule addedSchedule = Schedule.builder() .scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) .scheduleName("을사년 새해") @@ -1340,9 +1360,15 @@ void finishScheduleWithWrongUserId(){ .build(); // when // then - assertThatThrownBy(() -> scheduleService.finishSchedule(addedUser.getId() + 1, finishPreparationDto)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("존재하지 않는 유저 id입니다."); + assertThatThrownBy(() -> scheduleService.finishSchedule(otherUser.getId(), addedSchedule.getScheduleId(), finishPreparationDto)) + .isInstanceOf(GeneralException.class) + .hasMessage(ErrorCode.UNAUTHORIZED_ACCESS.getMessage()) + .extracting("errorCode") + .isEqualTo(ErrorCode.UNAUTHORIZED_ACCESS); + + assertThat(scheduleRepository.findById(addedSchedule.getScheduleId()).get().getLatenessTime()).isEqualTo(-1); + assertThat(userRepository.findById(addedUser.getId()).get().getPunctualityScore()).isEqualTo(-1f); + assertThat(userRepository.findById(otherUser.getId()).get().getPunctualityScore()).isEqualTo(-1f); } @DisplayName("약속을 종료할 때, 잘못된 scheduleId를 인자로 넘기는 경우 예외가 발생한다.") @@ -1374,9 +1400,62 @@ void finishScheduleWithWrongScheduleId(){ .build(); // when // then - assertThatThrownBy(() -> scheduleService.finishSchedule(addedUser.getId(), finishPreparationDto)) + assertThatThrownBy(() -> scheduleService.finishSchedule( + addedUser.getId(), + UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe6"), + finishPreparationDto + )) + .isInstanceOf(GeneralException.class) + .hasMessage(ErrorCode.SCHEDULE_NOT_FOUND.getMessage()) + .extracting("errorCode") + .isEqualTo(ErrorCode.SCHEDULE_NOT_FOUND); + + assertThat(scheduleRepository.findById(addedSchedule.getScheduleId()).get().getLatenessTime()).isEqualTo(-1); + assertThat(userRepository.findById(addedUser.getId()).get().getPunctualityScore()).isEqualTo(-1f); + } + + @DisplayName("이미 종료된 약속을 다시 종료하면 예외가 발생하고 성실도점수를 다시 계산하지 않는다.") + @Test + void finishScheduleWithAlreadyFinishedSchedule(){ + // given + User addedUser = User.builder() + .email("user@example.com") + .password(passwordEncoder.encode("password1234")) + .name("junbeom") + .punctualityScore(100f) + .scheduleCountAfterReset(1) + .latenessCountAfterReset(0) + .build(); + userRepository.save(addedUser); + + Schedule addedSchedule = Schedule.builder() + .scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) + .scheduleName("을사년 새해") + .scheduleTime(LocalDateTime.of(2025, 1, 1, 0, 0)) + .latenessTime(0) + .doneStatus(DoneStatus.NORMAL) + .user(addedUser) + .build(); + scheduleRepository.save(addedSchedule); + + FinishPreparationDto finishPreparationDto = FinishPreparationDto.builder() + .scheduleId(addedSchedule.getScheduleId()) + .latenessTime(1) + .build(); + + // when // then + assertThatThrownBy(() -> scheduleService.finishSchedule(addedUser.getId(), addedSchedule.getScheduleId(), finishPreparationDto)) .isInstanceOf(GeneralException.class) - .hasMessage("해당 약속이 존재하지 않습니다."); + .hasMessage(ErrorCode.SCHEDULE_ALREADY_FINISHED.getMessage()) + .extracting("errorCode") + .isEqualTo(ErrorCode.SCHEDULE_ALREADY_FINISHED); + + User user = userRepository.findById(addedUser.getId()).get(); + Schedule schedule = scheduleRepository.findById(addedSchedule.getScheduleId()).get(); + assertThat(schedule.getLatenessTime()).isEqualTo(0); + assertThat(user.getPunctualityScore()).isEqualTo(100f); + assertThat(user.getScheduleCountAfterReset()).isEqualTo(1); + assertThat(user.getLatenessCountAfterReset()).isEqualTo(0); } @Test