Skip to content

Commit 7043d77

Browse files
committed
Merge remote-tracking branch 'origin/main' into dev
# Conflicts: # ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java
2 parents 82f497f + c76fc7d commit 7043d77

52 files changed

Lines changed: 4981 additions & 41 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ jobs:
239239
timeout 5 bash -c "</dev/tcp/$DB_HOST/$DB_PORT"
240240
fi
241241
242-
echo "${{ secrets.GHCR_READ_TOKEN }}" | sudo docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin
242+
echo "${{ secrets.GITHUB_TOKEN }}" | sudo docker login ghcr.io -u "${{ github.actor }}" --password-stdin
243243
244244
if sudo docker compose version >/dev/null 2>&1; then
245245
sudo docker compose pull
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Ops Tail Production Logs
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: read
8+
9+
jobs:
10+
tail-logs:
11+
runs-on: ubuntu-latest
12+
environment: production
13+
steps:
14+
- name: Tail production backend logs
15+
uses: appleboy/ssh-action@v1.0.3
16+
with:
17+
host: ${{ secrets.EC2_HOST }}
18+
username: ${{ secrets.EC2_USER }}
19+
key: ${{ secrets.EC2_SSH_KEY }}
20+
script: |
21+
set -eu
22+
sudo docker logs --since 30m --tail=500 ontime-container \
23+
| grep -E -A40 -B10 'Apple login failed|InvalidTokenException|invalid_client|invalid_grant|audience|issuer|Exchange Apple credential|Verify Apple identity credential|/users/me/onboarding|onboarding|Onboarding|DataIntegrityViolation|MethodArgumentNotValidException|HttpMessageNotReadableException|GeneralException|Exception|ERROR|WARN' \
24+
|| true

.github/workflows/test.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,21 @@ jobs:
6161
6262
6363
64-
# 4. Gradle 빌드 & JUnit 테스트 실행
64+
# 4. Gradle 검증 및 커버리지 검사 실행
6565
- name: Run Tests with Gradle
6666
env:
6767
SPRING_PROFILES_ACTIVE: test
6868
run: |
6969
cd ontime-back
70-
./gradlew test
70+
./gradlew check
71+
72+
- name: Upload JaCoCo Coverage Report
73+
if: always()
74+
uses: actions/upload-artifact@v4
75+
with:
76+
name: jacoco-coverage-report
77+
path: ontime-back/build/reports/jacoco/test/
78+
if-no-files-found: warn
7179

7280
- name: Verify Flyway Migrations
7381
run: |

ontime-back/build.gradle

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
plugins {
22
id 'java'
3+
id 'jacoco'
34
id 'org.springframework.boot' version '3.3.4'
45
id 'io.spring.dependency-management' version '1.1.6'
56
id 'org.flywaydb.flyway' version '9.22.1'
@@ -70,4 +71,30 @@ dependencies {
7071

7172
tasks.named('test') {
7273
useJUnitPlatform()
74+
finalizedBy tasks.named('jacocoTestReport')
75+
}
76+
77+
tasks.named('jacocoTestReport') {
78+
dependsOn tasks.named('test')
79+
80+
reports {
81+
xml.required = true
82+
html.required = true
83+
}
84+
}
85+
86+
tasks.named('jacocoTestCoverageVerification') {
87+
dependsOn tasks.named('jacocoTestReport')
88+
89+
violationRules {
90+
rule {
91+
limit {
92+
minimum = 0.95
93+
}
94+
}
95+
}
96+
}
97+
98+
tasks.named('check') {
99+
dependsOn tasks.named('jacocoTestCoverageVerification')
73100
}

ontime-back/src/main/java/devkor/ontime_back/entity/PreparationSchedule.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class PreparationSchedule {
2424
@OnDelete(action = OnDeleteAction.CASCADE)
2525
private Schedule schedule;
2626

27-
@Column(nullable = false, length = 30)
27+
@Column(nullable = false, length = 50)
2828
private String preparationName;
2929

3030
private Integer preparationTime;

ontime-back/src/main/java/devkor/ontime_back/entity/PreparationUser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class PreparationUser {
2424
@OnDelete(action = OnDeleteAction.CASCADE)
2525
private User user;
2626

27-
@Column(nullable = false, length = 30)
27+
@Column(nullable = false, length = 50)
2828
private String preparationName;
2929

3030
private Integer preparationTime;

ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilter.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public void saveAuthentication(User myUser) {
125125
}
126126

127127
UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder()
128-
.username(myUser.getEmail())
128+
.username(authenticationName(myUser))
129129
.password(password)
130130
.roles(myUser.getRole().name())
131131
.build();
@@ -137,6 +137,14 @@ public void saveAuthentication(User myUser) {
137137
SecurityContextHolder.getContext().setAuthentication(authentication);
138138
}
139139

140+
private String authenticationName(User user) {
141+
String email = user.getEmail();
142+
if (email != null && !email.isBlank()) {
143+
return email;
144+
}
145+
return "user:" + user.getId();
146+
}
147+
140148
// 비밀번호 랜덤 생성
141149
private String generateRandomPassword(int length) {
142150
final String CHAR_SET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()-_=+[]{}|;:,.<>?";

ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginFilter.java

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,13 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
8181
}
8282

8383
String appleUserId = tokenClaims.getSubject();
84-
String email = tokenClaims.get("email", String.class);
84+
String email = firstNonBlank(
85+
oAuthAppleRequestDto.getEmail(),
86+
tokenClaims.get("email", String.class)
87+
);
88+
String appleRefreshToken = exchangeAppleRefreshToken(oAuthAppleRequestDto);
8589

86-
// socialRefreshtoken에 저장
87-
String appleRefreshToken = appleLoginService.getAppleAccessTokenAndRefreshToken(oAuthAppleRequestDto.getAuthCode()).getRefreshToken();
88-
89-
OAuthAppleUserDto oAuthAppleUserDto = new OAuthAppleUserDto(appleUserId, oAuthAppleRequestDto.getEmail(), oAuthAppleRequestDto.getFullName());
90+
OAuthAppleUserDto oAuthAppleUserDto = new OAuthAppleUserDto(appleUserId, email, oAuthAppleRequestDto.getFullName());
9091

9192
Optional<User> existingUser = userRepository.findBySocialTypeAndSocialId(SocialType.APPLE, appleUserId);
9293

@@ -97,19 +98,43 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
9798
}
9899

99100
} catch (Exception e) {
100-
log.error("Apple login failed: {}", e.getClass().getSimpleName());
101-
throw new AuthenticationException("Apple 로그인 실패") {};
101+
log.error("Apple login failed", e);
102+
throw new AppleLoginException();
102103
}
103104
}
104105

106+
private String exchangeAppleRefreshToken(OAuthAppleRequestDto oAuthAppleRequestDto) {
107+
try {
108+
AppleTokenResponseDto tokenResponse = appleLoginService.getAppleAccessTokenAndRefreshToken(
109+
oAuthAppleRequestDto.getAuthCode()
110+
);
111+
return tokenResponse.getRefreshToken();
112+
} catch (Exception e) {
113+
log.warn("Apple credential exchange failed; continuing with verified identity credential", e);
114+
return null;
115+
}
116+
}
117+
118+
private String firstNonBlank(String first, String second) {
119+
if (first != null && !first.isBlank()) {
120+
return first;
121+
}
122+
if (second != null && !second.isBlank()) {
123+
return second;
124+
}
125+
return null;
126+
}
127+
105128
@Override
106129
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
107130
AuthenticationException failed) throws IOException {
108131
if (failed instanceof RequestValidationException) {
109132
return;
110133
}
111134
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
112-
response.getWriter().write("{\"status\":\"error\", \"message\":\"Authentication failed\"}");
135+
response.setContentType("application/json");
136+
response.setCharacterEncoding("UTF-8");
137+
response.getWriter().write("{\"status\":\"error\", \"message\":\"Apple 로그인 실패\"}");
113138
}
114139

115140
private static class RequestValidationException extends AuthenticationException {
@@ -118,4 +143,10 @@ private RequestValidationException() {
118143
}
119144
}
120145

146+
private static class AppleLoginException extends AuthenticationException {
147+
private AppleLoginException() {
148+
super("Apple login failed");
149+
}
150+
}
151+
121152
}

ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginService.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ public class AppleLoginService {
5252

5353
private static final String APPLE_KEYS_URL = "https://appleid.apple.com/auth/keys";
5454
private static final String APPLE_TOKEN_URL = "https://appleid.apple.com/auth/token";
55-
private static final String REDIRECT_URI = "https://ontime-back.duckdns.org/oauth2/apple/callback";
5655
private String issuer = "https://appleid.apple.com";
5756
@Value("${apple.client.id}")
5857
private String clientId;
@@ -77,7 +76,9 @@ public class AppleLoginService {
7776
private final RestTemplate restTemplate = new RestTemplate();
7877
public Authentication handleLogin(String appleRefreshToken, User user, HttpServletResponse response) throws IOException {
7978
log.info("handleLogin");
80-
user.updateSocialLoginToken(appleRefreshToken);
79+
if (appleRefreshToken != null && !appleRefreshToken.isBlank()) {
80+
user.updateSocialLoginToken(appleRefreshToken);
81+
}
8182

8283
String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getId());
8384
String refreshToken = jwtTokenProvider.createRefreshToken();
@@ -134,7 +135,7 @@ public Authentication handleRegister(String appleRefreshToken, OAuthAppleUserDto
134135

135136
User savedUser = userRepository.save(newUser);
136137

137-
String accessToken = jwtTokenProvider.createAccessToken(newUser.getEmail(), newUser.getId());
138+
String accessToken = jwtTokenProvider.createAccessToken(savedUser.getEmail(), savedUser.getId());
138139
String refreshToken = jwtTokenProvider.createRefreshToken();
139140

140141
jwtTokenProvider.sendAccessAndRefreshToken(response, accessToken, refreshToken);
@@ -201,12 +202,7 @@ public AppleTokenResponseDto getAppleAccessTokenAndRefreshToken(String authCode)
201202
// clientSecret
202203
String clientSecret = generateClientSecret();
203204
log.info("Exchange Apple credential");
204-
MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();
205-
requestBody.add("grant_type", "authorization_code");
206-
requestBody.add("code", authCode);
207-
requestBody.add("client_id", clientId);
208-
requestBody.add("client_secret", clientSecret);
209-
requestBody.add("redirect_uri", REDIRECT_URI);
205+
MultiValueMap<String, String> requestBody = buildAppleTokenRequestBody(authCode, clientSecret);
210206

211207
HttpHeaders headers = new HttpHeaders();
212208
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
@@ -222,6 +218,15 @@ public AppleTokenResponseDto getAppleAccessTokenAndRefreshToken(String authCode)
222218
return objectMapper.treeToValue(response, AppleTokenResponseDto.class);
223219
}
224220

221+
MultiValueMap<String, String> buildAppleTokenRequestBody(String authCode, String clientSecret) {
222+
MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();
223+
requestBody.add("grant_type", "authorization_code");
224+
requestBody.add("code", authCode);
225+
requestBody.add("client_id", clientId);
226+
requestBody.add("client_secret", clientSecret);
227+
return requestBody;
228+
}
229+
225230
// clientsecret 생성
226231
private String generateClientSecret() throws Exception {
227232
log.info("Generate Apple client credential");

ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import jakarta.servlet.http.HttpServletResponse;
1919
import lombok.RequiredArgsConstructor;
2020
import lombok.extern.slf4j.Slf4j;
21+
import org.springframework.beans.factory.annotation.Autowired;
2122
import org.springframework.beans.factory.annotation.Value;
2223
import org.springframework.http.HttpEntity;
2324
import org.springframework.http.HttpHeaders;
@@ -50,19 +51,35 @@ public class GoogleLoginService {
5051
private static final String GOOGLE_REVOKE_URL = "https://oauth2.googleapis.com/revoke?token=";
5152

5253
private final List<String> validClientIds;
54+
private final RestTemplate revokeRestTemplate;
5355

56+
@Autowired
5457
public GoogleLoginService(
5558
JwtTokenProvider jwtTokenProvider,
5659
UserRepository userRepository,
5760
UserAlarmSettingRepository userAlarmSettingRepository,
5861
AnalyticsPreferenceService analyticsPreferenceService,
5962
@Value("${google.web.client-id}") String webClientId,
6063
@Value("${google.app.client-id}") String appClientId
64+
) {
65+
this(jwtTokenProvider, userRepository, userAlarmSettingRepository, analyticsPreferenceService,
66+
webClientId, appClientId, createRevokeRestTemplate());
67+
}
68+
69+
GoogleLoginService(
70+
JwtTokenProvider jwtTokenProvider,
71+
UserRepository userRepository,
72+
UserAlarmSettingRepository userAlarmSettingRepository,
73+
AnalyticsPreferenceService analyticsPreferenceService,
74+
String webClientId,
75+
String appClientId,
76+
RestTemplate revokeRestTemplate
6177
) {
6278
this.jwtTokenProvider = jwtTokenProvider;
6379
this.userRepository = userRepository;
6480
this.userAlarmSettingRepository = userAlarmSettingRepository;
6581
this.analyticsPreferenceService = analyticsPreferenceService;
82+
this.revokeRestTemplate = revokeRestTemplate;
6683
this.validClientIds = Stream.concat(
6784
Stream.of(webClientId),
6885
Stream.of(appClientId.split(","))
@@ -75,6 +92,13 @@ public GoogleLoginService(
7592
.toList());
7693
}
7794

95+
private static RestTemplate createRevokeRestTemplate() {
96+
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
97+
requestFactory.setConnectTimeout(3000);
98+
requestFactory.setReadTimeout(3000);
99+
return new RestTemplate(requestFactory);
100+
}
101+
78102

79103
public Authentication handleLogin(OAuthGoogleRequestDto oAuthGoogleRequestDto, User user, HttpServletResponse response) throws IOException {
80104
user.updateSocialLoginToken(oAuthGoogleRequestDto.getRefreshToken());
@@ -133,7 +157,7 @@ public Authentication handleRegister(OAuthGoogleRequestDto oAuthGoogleRequestDto
133157

134158
User savedUser = userRepository.save(newUser);
135159

136-
String accessToken = jwtTokenProvider.createAccessToken(newUser.getEmail(), newUser.getId());
160+
String accessToken = jwtTokenProvider.createAccessToken(savedUser.getEmail(), savedUser.getId());
137161
String refreshToken = jwtTokenProvider.createRefreshToken();
138162

139163
jwtTokenProvider.sendAccessAndRefreshToken(response, accessToken, refreshToken);
@@ -233,18 +257,14 @@ public boolean revokeToken(Long userId) {
233257

234258
String googleRefreshToken = user.getSocialLoginToken();
235259

236-
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
237-
requestFactory.setConnectTimeout(3000);
238-
requestFactory.setReadTimeout(3000);
239-
RestTemplate restTemplate = new RestTemplate(requestFactory);
240260
String revokeUrl = GOOGLE_REVOKE_URL + googleRefreshToken;
241261

242262
HttpHeaders headers = new HttpHeaders();
243263
headers.set("Content-Type", "application/x-www-form-urlencoded");
244264

245265
HttpEntity<String> request = new HttpEntity<>(headers);
246266

247-
ResponseEntity<String> response = restTemplate.exchange(
267+
ResponseEntity<String> response = revokeRestTemplate.exchange(
248268
revokeUrl, HttpMethod.POST, request, String.class);
249269

250270
return response.getStatusCode().is2xxSuccessful();

0 commit comments

Comments
 (0)