Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
name: Deploy Dev

on:
workflow_dispatch:
push:
branches:
- dev

permissions:
contents: read
packages: write

concurrency:
group: deploy-development
cancel-in-progress: true

env:
REGISTRY: ghcr.io
IMAGE_NAME: devkor-github/ontime-back
IMAGE_TAG: dev-${{ github.sha }}

jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@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:
context: ./ontime-back
file: ./ontime-back/Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev-latest
cache-from: type=gha
cache-to: type=gha,mode=max

deploy-to-remote-pc:
needs: build-and-push
runs-on: ubuntu-latest
environment: development
env:
DEV_DEPLOY_DIR: ${{ secrets.DEV_DEPLOY_DIR || format('/home/{0}/OnTime-back-dev', secrets.DEV_REMOTE_USER) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Prepare dev deploy directory
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEV_REMOTE_HOST }}
username: ${{ secrets.DEV_REMOTE_USER }}
key: ${{ secrets.DEV_REMOTE_SSH_KEY }}
script: |
set -eu
mkdir -p "${{ env.DEV_DEPLOY_DIR }}"

- name: Upload compose files to remote PC
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DEV_REMOTE_HOST }}
username: ${{ secrets.DEV_REMOTE_USER }}
key: ${{ secrets.DEV_REMOTE_SSH_KEY }}
source: "ontime-back/docker-compose.yml,ontime-back/docker-compose.dev.yml"
target: ${{ env.DEV_DEPLOY_DIR }}
strip_components: 1

- name: Pull image and restart dev containers
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEV_REMOTE_HOST }}
username: ${{ secrets.DEV_REMOTE_USER }}
key: ${{ secrets.DEV_REMOTE_SSH_KEY }}
script: |
set -eu

DEPLOY_DIR="${{ env.DEV_DEPLOY_DIR }}"
CONTAINER_NAME="ontime-dev-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-dev-container
BACKEND_HTTP_PORT=${{ secrets.DEV_BACKEND_HTTP_PORT || '8081' }}
BACKEND_MEMORY_LIMIT=${{ secrets.DEV_BACKEND_MEMORY_LIMIT || '768m' }}
BACKEND_CPU_LIMIT=${{ secrets.DEV_BACKEND_CPU_LIMIT || '1.0' }}
SERVER_PORT=8080
SPRING_PROFILES_ACTIVE=dev
JAVA_TOOL_OPTIONS=-XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom

MYSQL_DATABASE=${{ secrets.DEV_MYSQL_DATABASE || 'ontime_dev' }}
MYSQL_USER=${{ secrets.DEV_MYSQL_USER || 'ontime_dev' }}
MYSQL_PASSWORD=${{ secrets.DEV_MYSQL_PASSWORD || 'ontime_dev_password' }}
MYSQL_ROOT_PASSWORD=${{ secrets.DEV_MYSQL_ROOT_PASSWORD || 'ontime_dev_root_password' }}

SPRING_APPLICATION_NAME=${{ secrets.DEV_SPRING_APPLICATION_NAME || 'ontime-back-dev' }}
SPRING_DATASOURCE_URL=${{ secrets.DEV_SPRING_DATASOURCE_URL || 'jdbc:mysql://mysql:3306/ontime_dev?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true' }}
SPRING_DATASOURCE_USERNAME=${{ secrets.DEV_SPRING_DATASOURCE_USERNAME || secrets.DEV_MYSQL_USER || 'ontime_dev' }}
SPRING_DATASOURCE_PASSWORD=${{ secrets.DEV_SPRING_DATASOURCE_PASSWORD || secrets.DEV_MYSQL_PASSWORD || 'ontime_dev_password' }}
SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver
SPRING_JPA_HIBERNATE_DDL_AUTO=validate

SPRING_FLYWAY_ENABLED=true
SPRING_FLYWAY_BASELINE_ON_MIGRATE=false

JWT_SECRET_KEY=${{ secrets.DEV_JWT_SECRETKEY || 'dev_secret_key_for_ontime_back_remote_pc_development_1234567890' }}
JWT_ACCESS_EXPIRATION=${{ secrets.DEV_JWT_ACCESS_EXPIRATION || '3600000' }}
JWT_REFRESH_EXPIRATION=${{ secrets.DEV_JWT_REFRESH_EXPIRATION || '1209600000' }}
JWT_ACCESS_HEADER=${{ secrets.DEV_JWT_ACCESS_HEADER || 'Authorization' }}
JWT_REFRESH_HEADER=${{ secrets.DEV_JWT_REFRESH_HEADER || 'Authorization-refresh' }}

GOOGLE_WEB_CLIENT_ID=${{ secrets.DEV_GOOGLE_WEB_CLIENT_ID || 'dev-google-web-client-id' }}
GOOGLE_APP_CLIENT_ID=${{ secrets.DEV_GOOGLE_APP_CLIENT_ID || 'dev-google-app-client-id' }}

APPLE_CLIENT_ID=${{ secrets.DEV_APPLE_CLIENT_ID || 'dev-apple-client-id' }}
APPLE_TEAM_ID=${{ secrets.DEV_APPLE_TEAM_ID || 'dev-apple-team-id' }}
APPLE_LOGIN_KEY=${{ secrets.DEV_APPLE_LOGIN_KEY || 'dev-apple-key-id' }}
APPLE_PRIVATE_KEY_BASE64=${{ secrets.DEV_APPLE_PRIVATE_KEY_BASE64 }}
FEATURE_APPLE_LOGIN_ENABLED=${{ secrets.DEV_FEATURE_APPLE_LOGIN_ENABLED || 'false' }}

FIREBASE_CREDENTIALS_BASE64=${{ secrets.DEV_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
COMPOSE="sudo docker compose"
else
COMPOSE="sudo docker-compose"
fi

$COMPOSE -f docker-compose.yml -f docker-compose.dev.yml pull
$COMPOSE -f docker-compose.yml -f docker-compose.dev.yml up -d --remove-orphans

HEALTHY=false
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."
HEALTHY=true
break
fi
echo "Waiting for healthy container status; current status: ${STATUS:-unknown}"
sleep 5
done

if [ "$HEALTHY" != "true" ]; then
sudo docker logs --tail=200 "$CONTAINER_NAME" || true
exit 1
fi

curl -fsS "http://127.0.0.1:${{ secrets.DEV_BACKEND_HTTP_PORT || '8081' }}/actuator/health/readiness"
30 changes: 21 additions & 9 deletions docs/account-deletion-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ Frontend integration guide for deleting an OnTime account with optional withdraw

Account deletion hard-deletes the user from OnTime. The request can optionally include feedback. If feedback is provided, the backend stores it separately from the `User` table so it remains available after the account is deleted.

For Google and Apple social accounts, the backend first tries to revoke the social login token, then deletes the local OnTime account.
For Google and Apple social accounts, the backend first tries to revoke the social login token, then deletes the local OnTime account. If provider token revocation fails, the backend logs a warning and still deletes the local OnTime account.

For release/privacy evidence by data category, see `docs/account-deletion-verification-evidence.md`.

## Authentication

Expand Down Expand Up @@ -99,14 +101,19 @@ Content-Type: application/json

Success response:

```text
구글 로그인 회원탈퇴 성공
```json
{
"status": "success",
"code": "200",
"message": "구글 로그인 회원탈퇴 성공",
"data": null
}
```

Behavior:

- Revokes the stored Google OAuth token for OnTime.
- Deletes the local OnTime account.
- Deletes the local OnTime account even if Google token revocation fails.
- Saves optional feedback if `message` is nonblank.
- Does not delete the user's actual Google account.

Expand All @@ -118,7 +125,7 @@ Expected frontend result after successful revoke:
Important caveat:

- The Google unlink only works if the backend has a valid Google refresh/access token saved in `socialLoginToken`.
- If the client never provided a real Google refresh token, Google revoke may fail and the endpoint may return an error before local deletion.
- If the client never provided a real Google refresh token, Google revoke may fail. The backend logs the revoke failure and still deletes the local OnTime account.

## Apple Account Deletion

Expand All @@ -143,14 +150,19 @@ Content-Type: application/json

Success response:

```text
애플 로그인 회원탈퇴 성공
```json
{
"status": "success",
"code": "200",
"message": "애플 로그인 회원탈퇴 성공",
"data": null
}
```

Behavior:

- Revokes the stored Apple OAuth token for OnTime.
- Deletes the local OnTime account.
- Deletes the local OnTime account even if Apple token revocation fails.
- Saves optional feedback if `message` is nonblank.
- Does not delete the user's Apple ID.

Expand All @@ -177,4 +189,4 @@ Feedback is not linked by foreign key to the deleted user.
- Use `/oauth2/apple/me` for Apple-linked accounts if the product requirement is to unlink Apple access.
- Use `/users/me/delete` for normal local deletion.
- After a successful deletion response, clear local auth state and navigate to the logged-out screen.
- Do not retry automatically on social revoke errors without showing the user, because the local account may not have been deleted.
- If social provider revocation fails, the backend still returns success after deleting the local account. Follow up through support or backend logs if provider unlink confirmation is required.
42 changes: 42 additions & 0 deletions docs/account-deletion-request-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Account Deletion Request Page

Issue: #440

## Public URL

Use the production backend HTTPS host plus the stable path:

```text
https://3.38.172.54.nip.io/account-deletion
```

If the production backend is later moved to a custom domain, keep the same path
and update Google Play Console to the custom-domain URL.

## Hosting Surface

The page is hosted by the Spring Boot backend at `GET /account-deletion`.

It is public and does not require an OnTime access token. The path is explicitly
allowed in both Spring Security route authorization and the custom JWT filter
skip list.

## Request Handling Channel

Outside-app account deletion requests are handled by email:

```text
jjoonleo@gmail.com
```

The authenticated backend API remains available for in-app account deletion.

## Retention Enforcement

Backend-owned database retention is enforced for:

- Optional account deletion feedback: up to 1 year.
- Backend API logs: up to 90 days.

Backup rotation and external monitoring/security log retention must be enforced
by the production infrastructure and logging tools, not only by application code.
83 changes: 83 additions & 0 deletions docs/account-deletion-verification-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Account Deletion Verification Evidence

Issue: #439
Date: 2026-05-10
Status: implementation verified locally; release environment evidence pending

## Scope

This document records backend verification evidence for account deletion. It is
intended to support frontend release QA and privacy review, not to replace
release-environment database, log, analytics, monitoring, or backup checks.

## Verification Run

Command executed from `ontime-back`:

```bash
./gradlew test --rerun-tasks
```

Result: passed.

Relevant test coverage:

- `devkor.ontime_back.service.UserAuthServiceTest`
- `devkor.ontime_back.controller.UserAuthControllerTest`
- `devkor.ontime_back.controller.SocialAuthControllerTest`

## Provider Evidence Matrix

| Provider | Endpoint Called | Environment | Test Account ID | Request Time | Response | Re-login Fails? | Owner | Evidence Link |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Normal | `DELETE /users/me/delete` | Local test | Test fixture | 2026-05-10 | JSON success, message `계정이 성공적으로 삭제되었습니다!` | Not verified locally | Backend repo | `UserAuthControllerTest`, `UserAuthServiceTest` |
| Google | `DELETE /oauth2/google/me` | Local test | Test fixture | 2026-05-10 | JSON success, message `구글 로그인 회원탈퇴 성공` | Not verified locally | Backend repo | `SocialAuthControllerTest`, `UserAuthServiceTest` |
| Apple | `DELETE /oauth2/apple/me` | Local test | Test fixture | 2026-05-10 | JSON success, message `애플 로그인 회원탈퇴 성공` | Not verified locally | Backend repo | `SocialAuthControllerTest`, `UserAuthServiceTest` |

Release QA must replace local-test evidence with staging or
production-equivalent evidence that includes the real environment, account ID,
request timestamp, response, owner, and evidence link.

## Data Deletion And Retention Matrix

| Data Category | Backend Location | Deleted, Anonymized, Retained, or N/A | Retention Duration | Retention Reason | Verification Method | Owner |
| --- | --- | --- | --- | --- | --- | --- |
| User profile fields such as email, name, image, note, role, punctuality fields, and social identity | `user` table | Deleted in local integration test | N/A after deletion | Account deletion hard-delete | `UserAuthService.deleteUser` calls `userRepository.delete(user)`; test asserts deleted user row is absent | Backend repo; release env owner TBD |
| Password or auth credentials for normal accounts | `user.password` | Deleted in local integration test | N/A after deletion | Account deletion hard-delete | User row deletion removes password field | Backend repo; release env owner TBD |
| OAuth provider linkage for Google | `user.social_type`, `user.social_id`, `user.social_login_token`; Google revoke endpoint | Local linkage deleted; provider revoke attempted | Local linkage N/A after deletion; provider-side retention TBD | Local account deletion hard-delete; provider unlink depends on Google revoke result | Controller test verifies deletion continues when Google revoke throws | Backend repo; provider/env owner TBD |
| OAuth provider linkage for Apple | `user.social_type`, `user.social_id`, `user.social_login_token`; Apple revoke endpoint | Local linkage deleted; provider revoke attempted | Local linkage N/A after deletion; provider-side retention TBD | Local account deletion hard-delete; provider unlink depends on Apple revoke result | Controller test verifies deletion continues when Apple revoke throws | Backend repo; provider/env owner TBD |
| Access and refresh tokens | `user.access_token`, `user.refresh_token`, `user_device.session_access_token`, `user_device.session_refresh_token` | Deleted in local integration test | N/A after deletion | Account deletion hard-delete and device cascade | Test asserts user and device rows are absent | Backend repo; release env owner TBD |
| Device records and FCM tokens | `user.firebase_token`, `user_device`, `user_device.firebase_token` | Deleted in local integration test | N/A after deletion | Account deletion hard-delete and device cascade | Test asserts user device row is absent | Backend repo; release env owner TBD |
| Alarm settings and alarm status | `user_alarm_setting`, `user_alarm_status` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from user/device | Test asserts alarm setting and status rows are absent | Backend repo; release env owner TBD |
| Default preparation settings | `preparation_user` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from user | Test asserts preparation user rows are absent | Backend repo; release env owner TBD |
| Schedules | `schedule`, `notification_schedule` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from user and schedule | Test asserts schedule and notification schedule rows are absent | Backend repo; release env owner TBD |
| Schedule preparation steps | `preparation_schedule` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from schedule | Test asserts preparation schedule rows are absent | Backend repo; release env owner TBD |
| Spare time setting | `user.spare_time`, `user_setting` | Deleted in local integration test | N/A after deletion | Account deletion hard-delete and setting cascade | Test asserts user setting row is absent | Backend repo; release env owner TBD |
| General feedback sent through `/feedback` | `feedback` | Deleted in local integration test | N/A after deletion | Foreign-key cascade from user | Test asserts feedback rows are absent | Backend repo; release env owner TBD |
| Account deletion feedback sent in delete request body | `account_deletion_feedback` | Retained with anonymized email hash in local integration test, then scheduled for cleanup | Up to 1 year | Service quality review and deletion-related support issues | `saveAccountDeletionFeedback` stores deleted user ID, social type, SHA-256 normalized email hash, message, and created timestamp without a user foreign key; `RetentionCleanupService` deletes rows older than 1 year; tests assert retained row has no plaintext email and cleanup honors the cutoff | Backend repo |
| Backend API request logs | `api_log` | Retained as operational metadata, then scheduled for cleanup | Up to 90 days | Service operation and debugging | `LoggingAspect` stores route, method, actor, client IP, status, latency, and timestamp without request bodies; `RetentionCleanupService` deletes rows older than 90 days; tests assert cleanup honors the cutoff | Backend repo |
| Hosting logs, audit logs, crash logs, analytics, monitoring events, or security records | Hosting/logging/monitoring/analytics/security tools | TBD | TBD | TBD | Requires release environment/tooling review | Backend/environment owner |
| Backups or disaster recovery snapshots | Database backups/snapshots | TBD | TBD | TBD | Requires backup policy review | Backend/environment owner |

## Social Token Revocation Decision

Google and Apple deletion endpoints attempt provider token revocation before
local account deletion. If the provider revoke call throws, the controller logs a
warning and still calls `UserAuthService.deleteUser`.

This means a successful social deletion response verifies local OnTime account
deletion, but it does not prove provider-side unlink or revoke success. Provider
unlink must be checked separately with real Google and Apple test accounts in
the release environment.

## Remaining Release/Privacy Checks

- Run deletion in the release verification environment for normal, Google, and
Apple accounts.
- Attach database/API evidence for every provider row in the matrix.
- Confirm provider-side unlink/re-login behavior for Google and Apple.
- Confirm operational log, crash log, analytics, monitoring, and backup
retention behavior.
- Confirm hosting/application log, monitoring, analytics, audit, and security
event retention behavior outside the backend database.
- Compare the final evidence with the privacy policy draft before closing #439.
Loading
Loading