Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
405d32f
Merge pull request #256 from DevKor-github/main
jjjh02 Dec 27, 2025
2ca0d83
deploy: prepare ec2 hosting
jjoonleo May 4, 2026
4c0b6e7
Merge pull request #257 from DevKor-github/codex/ec2-hosting
jjoonleo May 4, 2026
0b601c0
deploy: run mysql with backend compose
jjoonleo May 4, 2026
cfc0fdf
docs: move ec2 deployment guide
jjoonleo May 4, 2026
68ea121
Merge pull request #258 from DevKor-github/codex/ec2-hosting
jjoonleo May 4, 2026
9833d60
deploy: preserve mysql volume explicitly
jjoonleo May 4, 2026
4a50fb3
deploy: rollback compose mysql
jjoonleo May 4, 2026
7dd842a
Merge pull request #259 from DevKor-github/codex/ec2-hosting
jjoonleo May 4, 2026
70cf533
Merge pull request #261 from DevKor-github/main
jjoonleo May 5, 2026
46347e6
Merge pull request #263 from DevKor-github/codex/account-deletion-fee…
jjoonleo May 5, 2026
ccdc434
Merge pull request #265 from DevKor-github/main
jjoonleo May 5, 2026
ca672c3
Handle duplicate Google social users
jjoonleo May 5, 2026
b94e731
Add social login uniqueness constraint
jjoonleo May 5, 2026
55a6098
Make social uniqueness migration idempotent
jjoonleo May 5, 2026
89032ce
Harden social account deletion responses
jjoonleo May 5, 2026
4e4fa23
Merge pull request #266 from DevKor-github/codex/handle-duplicate-goo…
jjoonleo May 5, 2026
520d3e1
Fix social login token storage
jjoonleo May 6, 2026
b03bc03
Merge pull request #267 from DevKor-github/codex/fix-social-login-tok…
jjoonleo May 6, 2026
4433a2e
Fix Flyway migration version conflict
jjoonleo May 6, 2026
1703012
Merge pull request #268 from DevKor-github/codex/fix-flyway-social-to…
jjoonleo May 6, 2026
f4b0ceb
Fix Firebase initialization lifecycle hook
jjoonleo May 6, 2026
56ac641
Merge pull request #269 from DevKor-github/codex/fix-firebase-initial…
jjoonleo May 6, 2026
1068c6c
Merge remote-tracking branch 'origin/main' into codexd/merge-main-int…
jjoonleo May 7, 2026
6e5769b
Merge pull request #294 from DevKor-github/codexd/merge-main-into-deploy
jjoonleo May 7, 2026
4d850f9
Harden production database configuration
jjoonleo May 7, 2026
036dccf
Merge pull request #295 from DevKor-github/codexd/harden-production-d…
jjoonleo May 7, 2026
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
50 changes: 44 additions & 6 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,11 @@ jobs:
SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }}
SPRING_DATASOURCE_USERNAME=${{ secrets.SPRING_DATASOURCE_USERNAME }}
SPRING_DATASOURCE_PASSWORD=${{ secrets.SPRING_DATASOURCE_PASSWORD }}
SPRING_DATASOURCE_DRIVER_CLASS_NAME=${{ secrets.SPRING_DATASOURCE_DRIVER_CLASS_NAME }}
SPRING_JPA_HIBERNATE_DDL_AUTO=${{ secrets.SPRING_JPA_HIBERNATE_DDL_AUTO }}
SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver
SPRING_JPA_HIBERNATE_DDL_AUTO=validate

SPRING_FLYWAY_ENABLED=true
SPRING_FLYWAY_URL=${{ secrets.SPRING_FLYWAY_URL }}
SPRING_FLYWAY_USER=${{ secrets.SPRING_FLYWAY_USER }}
SPRING_FLYWAY_PASSWORD=${{ secrets.SPRING_FLYWAY_PASSWORD }}
SPRING_FLYWAY_BASELINE_ON_MIGRATE=true
SPRING_FLYWAY_BASELINE_ON_MIGRATE=false

JWT_SECRET_KEY=${{ secrets.JWT_SECRETKEY }}
JWT_ACCESS_EXPIRATION=${{ secrets.JWT_ACCESS_EXPIRATION }}
Expand Down Expand Up @@ -139,6 +136,47 @@ jobs:
FIREBASE_CREDENTIALS_BASE64=${{ secrets.FIREBASE_CREDENTIALS_BASE64 }}
EOF

fail_deploy() {
echo "Unsafe production database configuration: $1" >&2
exit 1
}

get_env_value() {
grep -E "^$1=" .env | tail -n 1 | cut -d= -f2-
}

DB_URL="$(get_env_value SPRING_DATASOURCE_URL)"
DB_USERNAME="$(get_env_value SPRING_DATASOURCE_USERNAME)"
DB_PASSWORD="$(get_env_value SPRING_DATASOURCE_PASSWORD)"
DDL_AUTO="$(get_env_value SPRING_JPA_HIBERNATE_DDL_AUTO)"
FLYWAY_BASELINE="$(get_env_value SPRING_FLYWAY_BASELINE_ON_MIGRATE)"
NORMALIZED_DB_URL="$(printf '%s' "$DB_URL" | tr '[:upper:]' '[:lower:]')"
NORMALIZED_DB_USERNAME="$(printf '%s' "$DB_USERNAME" | tr '[:upper:]' '[:lower:]')"

[ -n "$DB_URL" ] || fail_deploy "SPRING_DATASOURCE_URL is required."
[ -n "$DB_USERNAME" ] || fail_deploy "SPRING_DATASOURCE_USERNAME is required."
[ -n "$DB_PASSWORD" ] || fail_deploy "SPRING_DATASOURCE_PASSWORD is required."
[ "$NORMALIZED_DB_USERNAME" != "root" ] || fail_deploy "SPRING_DATASOURCE_USERNAME must not be root."
[ "$DDL_AUTO" = "validate" ] || fail_deploy "SPRING_JPA_HIBERNATE_DDL_AUTO must be validate."
[ "$FLYWAY_BASELINE" = "false" ] || fail_deploy "SPRING_FLYWAY_BASELINE_ON_MIGRATE must be false."

case "$NORMALIZED_DB_URL" in
*allowpublickeyretrieval=true*) fail_deploy "SPRING_DATASOURCE_URL must not enable allowPublicKeyRetrieval." ;;
esac

case "$NORMALIZED_DB_URL" in
*createdatabaseifnotexist=true*) fail_deploy "SPRING_DATASOURCE_URL must not create databases at startup." ;;
esac

case "$NORMALIZED_DB_URL" in
*usessl=false*) fail_deploy "SPRING_DATASOURCE_URL must not disable TLS." ;;
esac

case "$NORMALIZED_DB_URL" in
*sslmode=required*|*sslmode=verify_ca*|*sslmode=verify_identity*) ;;
*) fail_deploy "SPRING_DATASOURCE_URL must declare sslMode=REQUIRED, VERIFY_CA, or VERIFY_IDENTITY." ;;
esac

echo "${{ secrets.GHCR_READ_TOKEN }}" | sudo docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin

if sudo docker compose version >/dev/null 2>&1; then
Expand Down
56 changes: 3 additions & 53 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,62 +62,12 @@ jobs:



# 4. 환경 변수 설정 파일 생성
- name: Create Config Files
run: |
mkdir -p ontime-back/src/main/resources
mkdir -p ontime-back/src/main/resources/key
echo "spring.application.name=${{ secrets.SPRING_APPLICATION_NAME }}" > ontime-back/src/main/resources/application.properties
echo "spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test_db?serverTimezone=UTC&useSSL=false" >> ontime-back/src/main/resources/application.properties
echo "spring.datasource.username=test_user" >> ontime-back/src/main/resources/application.properties
echo "spring.datasource.password=test_password" >> ontime-back/src/main/resources/application.properties
echo "spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver" >> ontime-back/src/main/resources/application.properties
echo "spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect" >> ontime-back/src/main/resources/application.properties
echo "spring.jpa.hibernate.ddl-auto=validate" >> ontime-back/src/main/resources/application.properties
echo "spring.sql.init.mode=always" >> ontime-back/src/main/resources/application.properties
echo "jwt.secret.key=${{ secrets.JWT_SECRETKEY }}" >> ontime-back/src/main/resources/application.properties
echo "jwt.access.expiration=${{ secrets.JWT_ACCESS_EXPIRATION }}" >> ontime-back/src/main/resources/application.properties
echo "jwt.refresh.expiration=${{ secrets.JWT_REFRESH_EXPIRATION }}" >> ontime-back/src/main/resources/application.properties
echo "jwt.access.header=${{ secrets.JWT_ACCESS_HEADER }}" >> ontime-back/src/main/resources/application.properties
echo "jwt.refresh.header=${{ secrets.JWT_REFRESH_HEADER }}" >> ontime-back/src/main/resources/application.properties
echo "google.web.client-id = ${{ secrets.GOOGLE_WEB_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties
echo "google.app.client-id = ${{ secrets.GOOGLE_APP_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.registration.google.client-secret=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.registration.google.scope=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_SCOPE }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.registration.google.redirect-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_REDIRECT_URI }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.registration.google.authorization-grant-type=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_AUTHORIZATION_GRANT_TYPE }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.registration.google.client-name=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_NAME }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.provider.google.authorization-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_AUTHORIZATION_URI }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.provider.google.token-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_TOKEN_URI }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.provider.google.user-info-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_INFO_URI }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.provider.google.user-name-attribute=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_NAME_ATTRIBUTE }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.registration.kakao.client-id=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.registration.kakao.scope=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_SCOPE }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.registration.kakao.redirect-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_REDIRECT_URI }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.registration.kakao.authorization-grant-type=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_AUTHORIZATION_GRANT_TYPE }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.registration.kakao.client-name=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_NAME }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.provider.kakao.authorization-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_AUTHORIZATION_URI }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.provider.kakao.token-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_TOKEN_URI }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.provider.kakao.user-info-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_INFO_URI }}" >> ontime-back/src/main/resources/application.properties
echo "spring.security.oauth2.client.provider.kakao.user-name-attribute=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_NAME_ATTRIBUTE }}" >> ontime-back/src/main/resources/application.properties
echo "apple.client.id=${{ secrets.APPLE_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties
echo "apple.client.secret=${{ secrets.APPLE_CLIENT_SECRET }}" >> ontime-back/src/main/resources/application.properties
echo "apple.login.key=${{ secrets.APPLE_LOGIN_KEY }}" >> ontime-back/src/main/resources/application.properties
echo "apple.team.id=${{ secrets.APPLE_TEAM_ID }}" >> ontime-back/src/main/resources/application.properties
echo "spring.flyway.enabled=true" >> ontime-back/src/main/resources/application.properties
echo "spring.flyway.url=jdbc:mysql://127.0.0.1:3306/test_db?serverTimezone=UTC&useSSL=false" >> ontime-back/src/main/resources/application.properties
echo "spring.flyway.user=test_user" >> ontime-back/src/main/resources/application.properties
echo "spring.flyway.password=test_password" >> ontime-back/src/main/resources/application.properties
echo "spring.flyway.baseline-on-migrate=true" >> ontime-back/src/main/resources/application.properties
echo "management.endpoints.web.exposure.include=health" >> ontime-back/src/main/resources/application.properties
echo "management.endpoint.health.show-details=always" >> ontime-back/src/main/resources/application.properties
echo "${{ secrets.ONTIME_PUSH_FIREBASE_ADMINSDK }}" > ontime-back/src/main/resources/ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json
echo "${{ secrets.AUTHKEY_743M7R5W3W }}" > ontime-back/src/main/resources/key/AuthKey_743M7R5W3W.p8

# 5. Gradle 빌드 & JUnit 테스트 실행
# 4. Gradle 빌드 & JUnit 테스트 실행
- name: Run Tests with Gradle
id: run-tests # 실행 결과를 output으로 저장할 id 추가
continue-on-error: true
env:
SPRING_PROFILES_ACTIVE: test
run: |
cd ontime-back
./gradlew test
Expand Down
16 changes: 16 additions & 0 deletions ontime-back/docs/database-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Database Configuration

Production uses MySQL with environment-provided database credentials.

## Production

- Spring profile: `prod`
- Datasource URL: `SPRING_DATASOURCE_URL`
- Datasource username: `SPRING_DATASOURCE_USERNAME`
- Datasource password: `SPRING_DATASOURCE_PASSWORD`
- Datasource driver: `com.mysql.cj.jdbc.Driver`
- Hibernate DDL mode: `validate`
- SQL logging: disabled
- Formatted SQL logging: disabled
- Flyway: enabled
- Flyway baseline on migrate: disabled
60 changes: 60 additions & 0 deletions ontime-back/docs/deployment/ec2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# EC2 Deployment

This service deploys to Amazon EC2 through `.github/workflows/deploy.yml`.

## How to Deploy

1. Make sure the EC2 instance has Docker installed and the security group allows inbound traffic for the service port, currently `8080`.
2. Add the required GitHub Actions secrets listed below.
3. Run the `Deploy` workflow manually from GitHub Actions, or push to the `deploy` branch.

The workflow builds the Spring Boot jar, creates deploy-only config files from GitHub Secrets, uploads them to `/home/ubuntu/OnTime-back`, and restarts Docker Compose on the EC2 instance.

## Required EC2 Secrets

- `EC2_HOST`
- `EC2_USER`
- `EC2_SSH_KEY`

## Required Application Secrets

- `SPRING_APPLICATION_NAME`
- `SPRING_DATASOURCE_URL`
- `SPRING_DATASOURCE_USERNAME`
- `SPRING_DATASOURCE_PASSWORD`
- `SPRING_DATASOURCE_DRIVER_CLASS_NAME`
- `SPRING_JPA_HIBERNATE_DDL_AUTO`
- `JWT_SECRETKEY`
- `JWT_ACCESS_EXPIRATION`
- `JWT_REFRESH_EXPIRATION`
- `JWT_ACCESS_HEADER`
- `JWT_REFRESH_HEADER`
- `GOOGLE_WEB_CLIENT_ID`
- `GOOGLE_APP_CLIENT_ID`
- `APPLE_CLIENT_ID`
- `APPLE_LOGIN_KEY`
- `APPLE_TEAM_ID`
- `AUTHKEY_743M7R5W3W`
- `SPRING_FLYWAY_URL`
- `SPRING_FLYWAY_USER`
- `SPRING_FLYWAY_PASSWORD`
- `ONTIME_PUSH_FIREBASE_ADMINSDK`

## Optional Secrets

- `SPRING_JPA_DATABASE_PLATFORM` defaults to `org.hibernate.dialect.MySQL8Dialect`.
- `FEATURE_APPLE_LOGIN_ENABLED` defaults to `true`.
- Google and Kakao OAuth provider/registration secrets are included by the workflow when configured.

## Runtime Files on EC2

The deploy workflow writes these files under `/home/ubuntu/OnTime-back`:

- `project.jar`
- `Dockerfile`
- `docker-compose.yml`
- `config/application.properties`
- `secrets/firebase-adminsdk.json`
- `secrets/AuthKey_743M7R5W3W.p8`

Do not commit local `application.properties`, Firebase service account JSON, Apple `.p8` keys, or `.env` files.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import devkor.ontime_back.dto.OAuthKakaoUserDto;
import devkor.ontime_back.global.oauth.apple.AppleLoginService;
import devkor.ontime_back.global.oauth.google.GoogleLoginService;
import devkor.ontime_back.response.ApiResponseForm;
import devkor.ontime_back.service.UserAuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
Expand All @@ -16,6 +17,7 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Slf4j
Expand Down Expand Up @@ -113,24 +115,32 @@ public String appleRegisterOrLogin(@RequestBody OAuthAppleRequestDto appleLoginR
summary = "애플 소셜 로그인 회원탈퇴"
)
@DeleteMapping("/apple/me")
public String appleDeleteUser(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) FeedbackAddDto feedbackAddDto) throws Exception {
public ResponseEntity<ApiResponseForm<?>> appleDeleteUser(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) FeedbackAddDto feedbackAddDto) {
Long userId = userAuthService.getUserIdFromToken(request);
log.info("userId: {}", userId);
appleLoginService.revokeToken(userId);
try {
appleLoginService.revokeToken(userId);
} catch (Exception e) {
log.warn("Apple 토큰 철회에 실패했지만 계정 삭제를 계속 진행합니다. userId={}, reason={}", userId, e.getMessage());
}
userAuthService.deleteUser(userId, feedbackAddDto);
return "애플 로그인 회원탈퇴 성공";
return ResponseEntity.ok(ApiResponseForm.success(null, "애플 로그인 회원탈퇴 성공"));
}

@Operation(
summary = "구글 소셜 로그인 회원탈퇴"
)
@DeleteMapping("/google/me")
public String googleDeleteUser(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) FeedbackAddDto feedbackAddDto) throws Exception {
public ResponseEntity<ApiResponseForm<?>> googleDeleteUser(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) FeedbackAddDto feedbackAddDto) {
Long userId = userAuthService.getUserIdFromToken(request);
log.info("userId: {}", userId);
googleLoginService.revokeToken(userId);
try {
googleLoginService.revokeToken(userId);
} catch (Exception e) {
log.warn("Google 토큰 철회에 실패했지만 계정 삭제를 계속 진행합니다. userId={}, reason={}", userId, e.getMessage());
}
userAuthService.deleteUser(userId, feedbackAddDto);
return "구글 로그인 회원탈퇴 성공";
return ResponseEntity.ok(ApiResponseForm.success(null, "구글 로그인 회원탈퇴 성공"));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public class User {

private String firebaseToken;

@Lob
@Column(columnDefinition = "LONGTEXT")
private String socialLoginToken;

@OneToOne(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL)
Expand Down Expand Up @@ -134,4 +136,4 @@ public void updateFirebaseToken(String firebaseToken) {
public void updateAccessToken(String accessToken) {
this.accessToken = accessToken;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@ public class LoginService implements UserDetailsService {
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("해당 이메일이 존재하지 않습니다."));

System.out.println("유저임다: "+user);

return org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword())
.roles(user.getRole().name())
.build();
}
}
}
Loading
Loading