-
Notifications
You must be signed in to change notification settings - Fork 0
feat: supabase 소셜 로그인 토큰 교환 #176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.example.cp_main_be.domain.member.auth.dto.request; | ||
|
|
||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| public class SupabaseLoginRequest { | ||
| private String accessToken; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,9 +13,13 @@ | |
| import com.example.cp_main_be.global.common.CustomApiException; | ||
| import com.example.cp_main_be.global.common.ErrorCode; | ||
| import com.example.cp_main_be.global.jwt.JwtTokenProvider; | ||
| import com.example.cp_main_be.global.supabase.SupabaseAuthClient; | ||
| import com.example.cp_main_be.global.supabase.SupabaseUserResponse; | ||
| import java.time.LocalDateTime; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import java.util.UUID; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.slf4j.Logger; | ||
|
|
@@ -33,6 +37,7 @@ public class AuthService { | |
| private final Logger logger = LoggerFactory.getLogger(AuthService.class); | ||
| private final WishTreeService wishTreeService; | ||
| private final GardenRepository gardenRepository; | ||
| private final SupabaseAuthClient supabaseAuthClient; | ||
|
|
||
| /** 리프레시 토큰으로 액세스 토큰 재발급 + (권장) 리프레시 토큰 롤링 */ | ||
| @Transactional | ||
|
|
@@ -87,7 +92,68 @@ public AnonymousRegistrationResponse registerNewUser( | |
| .gardens(new ArrayList<>()) | ||
| .build(); | ||
|
|
||
| // 1. 사용자를 먼저 저장합니다. | ||
| User savedUser = saveAndInitializeUser(newUser); | ||
| return issueTokens(savedUser, true, deviceId); | ||
| } | ||
|
|
||
| @Transactional | ||
| public AnonymousRegistrationResponse loginWithSupabase(String accessToken, String deviceId) { | ||
| SupabaseUserResponse supabaseUser = supabaseAuthClient.fetchUser(accessToken); | ||
|
|
||
|
Comment on lines
+99
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find SupabaseAuthClient.java
file=$(fd -a 'SupabaseAuthClient.java' 2>/dev/null | head -n 1)
if [ -n "$file" ]; then
echo "=== SupabaseAuthClient.java ==="
wc -l "$file"
cat -n "$file"
else
echo "SupabaseAuthClient.java not found, searching for similar files..."
fd -a '.*Supabase.*' -t f | head -20
fiRepository: Const-Project/MainBE Length of output: 1992 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find and examine AuthService.java
file=$(fd -a 'AuthService.java' 2>/dev/null | grep -E 'auth/service' | head -n 1)
if [ -n "$file" ]; then
echo "=== AuthService.java (around lines 99-102) ==="
sed -n '85,120p' "$file"
echo ""
echo "=== Full method context (lines 90-150) ==="
sed -n '90,150p' "$file"
else
echo "AuthService.java not found"
fiRepository: Const-Project/MainBE Length of output: 3366 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Search for external HTTP client usage patterns in the codebase
echo "=== Searching for HTTP client patterns ==="
rg '(RestTemplate|WebClient|HttpClient|OkHttpClient|CloseableHttpClient)' -t java --max-count 20
echo ""
echo "=== Searching for Supabase client usage ==="
rg 'supabaseAuthClient' -t java -B 2 -A 2 | head -50Repository: Const-Project/MainBE Length of output: 2725 외부 Supabase 호출을 트랜잭션 밖으로 분리하세요
✅ 분리 예시- `@Transactional`
- public AnonymousRegistrationResponse loginWithSupabase(String accessToken, String deviceId) {
- SupabaseUserResponse supabaseUser = supabaseAuthClient.fetchUser(accessToken);
- ...
- }
+ public AnonymousRegistrationResponse loginWithSupabase(String accessToken, String deviceId) {
+ SupabaseUserResponse supabaseUser = supabaseAuthClient.fetchUser(accessToken);
+ return loginWithSupabaseInternal(supabaseUser, deviceId);
+ }
+
+ `@Transactional`
+ protected AnonymousRegistrationResponse loginWithSupabaseInternal(
+ SupabaseUserResponse supabaseUser, String deviceId) {
+ // 기존 조회/저장/토큰발급 로직 이동
+ }🤖 Prompt for AI Agents |
||
| String oauthSubject = supabaseUser.getId(); | ||
| String oauthProvider = extractProvider(supabaseUser); | ||
| if (oauthSubject == null || oauthProvider == null || oauthProvider.isBlank()) { | ||
| throw new CustomApiException(ErrorCode.INVALID_REQUEST); | ||
| } | ||
|
|
||
| Optional<User> existing = | ||
| userRepository.findByOauthProviderAndOauthSubject(oauthProvider, oauthSubject); | ||
|
|
||
| if (existing.isPresent()) { | ||
| User user = existing.get(); | ||
| boolean updated = applyProfileUpdates(user, supabaseUser); | ||
| if (updated) { | ||
| userRepository.save(user); | ||
| } | ||
| return issueTokens(user, false, deviceId); | ||
| } | ||
|
|
||
| String nickname = buildUniqueNickname(supabaseUser); | ||
| UUID newUuid = UUID.randomUUID(); | ||
|
|
||
| User newUser = | ||
| User.builder() | ||
| .uuid(newUuid) | ||
| .nickname(nickname) | ||
| .email(supabaseUser.getEmail()) | ||
| .profileImageUrl(extractProfileImageUrl(supabaseUser)) | ||
| .oauthProvider(oauthProvider) | ||
| .oauthSubject(oauthSubject) | ||
|
Comment on lines
+124
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Find User entity file
fd -a 'User\.java' --type f | head -5Repository: Const-Project/MainBE Length of output: 155 🏁 Script executed: # Check User entity definition
find . -name 'User.java' -type f | head -1 | xargs wc -lRepository: Const-Project/MainBE Length of output: 142 🏁 Script executed: # Find migration files
fd -a 'V.*\.sql' --type f | head -20Repository: Const-Project/MainBE Length of output: 318 🏁 Script executed: # Search for email/nickname in migration files
rg -n "nickname|email" --type sql -g '*.sql' | head -30Repository: Const-Project/MainBE Length of output: 788 🏁 Script executed: cat -n /home/jailuser/git/src/main/java/com/example/cp_main_be/domain/member/user/domain/User.javaRepository: Const-Project/MainBE Length of output: 6225 🏁 Script executed: cat /home/jailuser/git/src/main/resources/db/migration/V4__add_user_oauth_fields.sqlRepository: Const-Project/MainBE Length of output: 195 🏁 Script executed: # Check for null email handling in AuthService
rg -n "email|nickname" src/main/java/com/example/cp_main_be/domain/member/auth/service/AuthService.java -A 3 -B 1Repository: Const-Project/MainBE Length of output: 1156 🏁 Script executed: rg -n "buildUniqueNickname" src/main/java/com/example/cp_main_be/domain/member/auth/service/AuthService.java -A 15Repository: Const-Project/MainBE Length of output: 1289 🏁 Script executed: # Check all migration files to see actual nickname column definitions
cat /home/jailuser/git/src/main/resources/db/migration/V1__initial_schema.sql 2>/dev/null | grep -A 2 -B 2 "nickname\|email"Repository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Find the initial schema or all migration files content
fd -a '\.sql$' src/main/resources/db/migration/ | xargs ls -lhRepository: Const-Project/MainBE Length of output: 462 🏁 Script executed: rg -n "buildUniqueNickname" src/main/java/com/example/cp_main_be/domain/member/auth/service/AuthService.java -A 25Repository: Const-Project/MainBE Length of output: 1955 🏁 Script executed: # Search for the initial schema definition in migration files
rg -n "CREATE TABLE.*users" src/main/resources/db/migration/ -A 15Repository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Check if there's a schema.sql or init script
fd -a '\.(sql|ddl)$' src/main/resources/Repository: Const-Project/MainBE Length of output: 318 🏁 Script executed: # Search more broadly for users table creation
rg -i "create.*table" src/main/resources/db/migration/ -A 10Repository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Check all files in resources
find src/main/resources -type f | head -20Repository: Const-Project/MainBE Length of output: 327 🏁 Script executed: # Look for schema files or flyway initialization
fd '(schema|initial|ddl)' src/main/resources/ -iRepository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Check application.yml for Hibernate DDL generation settings
cat src/main/resources/application.yml | grep -i "hibernate\|jpa\|ddl"Repository: Const-Project/MainBE Length of output: 390 🏁 Script executed: # Look for entity-schema.sql or similar that was shown in earlier results
find . -name "*.sql" -type f 2>/dev/nullRepository: Const-Project/MainBE Length of output: 304 🏁 Script executed: # Check if nickname length constraint exists in User entity or any validation annotations
rg -n "@(Column|Size|Length)" src/main/java/com/example/cp_main_be/domain/member/user/domain/User.javaRepository: Const-Project/MainBE Length of output: 367 🏁 Script executed: # Get the complete buildUniqueNickname method to understand length constraints
rg -n "buildUniqueNickname|randomSuffix" src/main/java/com/example/cp_main_be/domain/member/auth/service/AuthService.java -A 30 | head -50Repository: Const-Project/MainBE Length of output: 1840 🏁 Script executed: # Check the generated entity-schema.sql for actual nickname column definition
grep -n "CREATE TABLE.*users" entity-schema.sql -A 20Repository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Check new-schema.sql as well
grep -n "CREATE TABLE.*users" new-schema.sql -A 20Repository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Get the complete buildUniqueNickname method with randomSuffix
rg -n "private String buildUniqueNickname|private.*randomSuffix" src/main/java/com/example/cp_main_be/domain/member/auth/service/AuthService.java -A 40 | tail -80Repository: Const-Project/MainBE Length of output: 1542 🏁 Script executed: # Check for any validation of email being null
rg -n "getEmail\|email.*null" src/main/java/com/example/cp_main_be/domain/member/auth/service/AuthService.javaRepository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Read entity-schema.sql directly
head -50 entity-schema.sqlRepository: Const-Project/MainBE Length of output: 1550 🏁 Script executed: # Look specifically for users table in entity-schema.sql
sed -n '/CREATE TABLE.*users/,/);/p' entity-schema.sql | head -30Repository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Check new-schema.sql
head -50 new-schema.sqlRepository: Const-Project/MainBE Length of output: 2272 🏁 Script executed: # Search for where avatar nickname is set
rg -n "avatar.*nickname\|Avatar.*nickname" src/main/java/com/example/cp_main_be/domain/ -B 2 -A 2Repository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Check if avatar creation also uses buildUniqueNickname
rg -n "buildUniqueNickname\|new Avatar" src/main/java/com/example/cp_main_be/ -B 2 -A 2Repository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Verify the exact length limit - check if buildUniqueNickname can produce strings over 255 chars
rg -n "class Avatar" src/main/java/com/example/cp_main_be/domain/ -A 30 | grep -i "nickname"Repository: Const-Project/MainBE Length of output: 855 🏁 Script executed: # Check if there's any null check or validation for email before saving
rg -n "saveAndInitializeUser" src/main/java/com/example/cp_main_be/domain/member/auth/service/AuthService.java -A 10Repository: Const-Project/MainBE Length of output: 1606 🏁 Script executed: # Check if there's any length constraint in SupabaseUserResponse
rg -n "class SupabaseUserResponse" src/main/java/com/example/cp_main_be/ -A 30Repository: Const-Project/MainBE Length of output: 1203 🏁 Script executed: # Verify if buildUniqueNickname has any length truncation logic
rg -n "substring\|length\|truncate" src/main/java/com/example/cp_main_be/domain/member/auth/service/AuthService.javaRepository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Check if there are any validators on User entity
rg -n "@(NotNull|NotBlank|Size|Length)" src/main/java/com/example/cp_main_be/domain/member/user/domain/User.javaRepository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Check what happens when email is null - any null coalescing or default values
rg -n "email.*\?" src/main/java/com/example/cp_main_be/domain/member/auth/service/AuthService.javaRepository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Verify if buildUniqueNickname handles extremely long input from userMetadata
rg -n "userMetadata.get\|firstNonBlank" src/main/java/com/example/cp_main_be/domain/member/auth/service/AuthService.java -B 2 -A 5Repository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Let's check if there are any tests that show how email null is handled
rg -n "loginWithSupabase\|email.*null" src/test/ -iRepository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Final verification: check actual limit by calculating max nickname length
# buildUniqueNickname: sanitized (no length limit) + randomSuffix (6 chars)
# Let's see if there's any actual limit check anywhere
rg -n "nickname.*length\|nickname.*size\|nickname.*\d+" src/main/java/com/example/cp_main_be/domain/member/Repository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Check if V1 migration exists or if Hibernate ddl-auto is creating tables
ls -la src/main/resources/db/migration/ | grep -i v1Repository: Const-Project/MainBE Length of output: 46 🏁 Script executed: # Let me verify the exact issue - check if there's any string length limit in buildUniqueNickname
python3 << 'EOF'
# Simulate buildUniqueNickname behavior
def simulate_nickname(metadata_value):
base = metadata_value if metadata_value else "user"
sanitized = base.replace(" ", "")
# Remove special chars (keep only alphanumeric, ._-)
sanitized = ''.join(c for c in sanitized if c.isalnum() or c in '._-')
if not sanitized:
sanitized = "user"
# Add suffix
suffix = "ABCDEF" # Simulating 6-char random suffix
candidate = sanitized + suffix
return candidate
# Test cases
test_cases = [
"user",
"john_doe",
"a" * 250, # Very long nickname
"한글이름테스트", # Korean characters (would be filtered)
]
for test in test_cases:
result = simulate_nickname(test)
print(f"Input: {test[:30]:<30} | Output length: {len(result):<5} | DB limit (varchar(255)): {len(result) <= 255}")
EOFRepository: Const-Project/MainBE Length of output: 436 🏁 Script executed: # Check database constraints one more time
cat new-schema.sql | grep -A 5 "create table users"Repository: Const-Project/MainBE Length of output: 346 buildUniqueNickname()에서 닉네임이 255자를 초과할 수 있습니다. 길이 제한 및 null 이메일 처리 로직을 추가하세요. User 엔티티의 nickname 필드에는 길이 제약이 없으며, 🤖 Prompt for AI Agents |
||
| .avatarList(new ArrayList<>()) | ||
| .diaries(new ArrayList<>()) | ||
| .gardens(new ArrayList<>()) | ||
| .build(); | ||
|
|
||
| User savedUser = saveAndInitializeUser(newUser); | ||
| return issueTokens(savedUser, true, deviceId); | ||
| } | ||
|
|
||
| /** 특정 리프레시 토큰 무효화(로그아웃) */ | ||
| public void revokeRefreshToken(String refreshToken) { | ||
| refreshTokenRepository.deleteByToken(refreshToken); | ||
| } | ||
|
|
||
| /** 해당 유저의 전체 리프레시 토큰 무효화(강제 로그아웃 All) */ | ||
| public void revokeAllByUser(UUID userUuid) { | ||
| refreshTokenRepository.deleteAllByUserUuid(userUuid); | ||
| } | ||
|
|
||
| /** 만료된 리프레시 토큰 청소 (스케쥴러로 주기적으로 호출) */ | ||
| public void purgeExpiredTokens() { | ||
| refreshTokenRepository.deleteAllByExpiresAtBefore(LocalDateTime.now()); | ||
| } | ||
|
|
||
| private User saveAndInitializeUser(User newUser) { | ||
| User savedUser = userRepository.save(newUser); | ||
|
|
||
| Garden firstGarden = | ||
|
|
@@ -100,20 +166,20 @@ public AnonymousRegistrationResponse registerNewUser( | |
| Garden.builder().user(savedUser).slotNumber(4).isLocked(true).build(); // 4번은 잠김 | ||
|
|
||
| gardenRepository.saveAll(List.of(firstGarden, secondGarden, thirdGarden, fourthGarden)); | ||
|
|
||
| // 2. 위시트리 관련 로직을 수행합니다. | ||
| // 만약 여기서 예외가 발생하면, 위에서 저장한 newUser까지 모두 롤백됩니다. | ||
| wishTreeService.addPointsToWishTree(savedUser.getId(), 0L); | ||
|
|
||
| // 3. 모든 것이 성공했을 때만 토큰을 생성하고 저장합니다. | ||
| String accessToken = jwtTokenProvider.generateAccessToken(newUuid.toString()); | ||
| String refreshToken = jwtTokenProvider.generateRefreshToken(newUuid.toString()); | ||
| return savedUser; | ||
| } | ||
|
|
||
| private AnonymousRegistrationResponse issueTokens(User user, boolean isNewUser, String deviceId) { | ||
| String accessToken = jwtTokenProvider.generateAccessToken(user.getUuid().toString()); | ||
| String refreshToken = jwtTokenProvider.generateRefreshToken(user.getUuid().toString()); | ||
| LocalDateTime expiry = jwtTokenProvider.getExpirationLocalDateTime(refreshToken); | ||
|
|
||
| RefreshToken rt = | ||
| RefreshToken.builder() | ||
| .token(refreshToken) | ||
| .userUuid(newUuid) | ||
| .userUuid(user.getUuid()) | ||
| .expiresAt(expiry) | ||
| .deviceId(deviceId) | ||
| .build(); | ||
|
|
@@ -122,24 +188,106 @@ public AnonymousRegistrationResponse registerNewUser( | |
| return AnonymousRegistrationResponse.builder() | ||
| .accessToken(accessToken) | ||
| .refreshToken(refreshToken) | ||
| .userId(savedUser.getId()) // save() 후 반환된 객체의 ID 사용 | ||
| .nickname(nickname) | ||
| .isNewUser(true) | ||
| .userId(user.getId()) | ||
| .nickname(user.getNickname()) | ||
| .isNewUser(isNewUser) | ||
| .build(); | ||
| } | ||
|
|
||
| /** 특정 리프레시 토큰 무효화(로그아웃) */ | ||
| public void revokeRefreshToken(String refreshToken) { | ||
| refreshTokenRepository.deleteByToken(refreshToken); | ||
| private String extractProvider(SupabaseUserResponse supabaseUser) { | ||
| Map<String, Object> appMetadata = supabaseUser.getAppMetadata(); | ||
| if (appMetadata == null) { | ||
| return null; | ||
| } | ||
| Object provider = appMetadata.get("provider"); | ||
| if (provider instanceof String providerStr && !providerStr.isBlank()) { | ||
| return providerStr; | ||
| } | ||
| Object providers = appMetadata.get("providers"); | ||
| if (providers instanceof List<?> providerList && !providerList.isEmpty()) { | ||
| Object first = providerList.get(0); | ||
| if (first instanceof String firstProvider && !firstProvider.isBlank()) { | ||
| return firstProvider; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** 해당 유저의 전체 리프레시 토큰 무효화(강제 로그아웃 All) */ | ||
| public void revokeAllByUser(UUID userUuid) { | ||
| refreshTokenRepository.deleteAllByUserUuid(userUuid); | ||
| private boolean applyProfileUpdates(User user, SupabaseUserResponse supabaseUser) { | ||
| boolean updated = false; | ||
|
|
||
| if (user.getEmail() == null && supabaseUser.getEmail() != null) { | ||
| user.setEmail(supabaseUser.getEmail()); | ||
| updated = true; | ||
| } | ||
|
|
||
| String profileImageUrl = extractProfileImageUrl(supabaseUser); | ||
| if (user.getProfileImageUrl() == null && profileImageUrl != null) { | ||
| user.setProfileImageUrl(profileImageUrl); | ||
| updated = true; | ||
| } | ||
|
|
||
| return updated; | ||
| } | ||
|
|
||
| /** 만료된 리프레시 토큰 청소 (스케쥴러로 주기적으로 호출) */ | ||
| public void purgeExpiredTokens() { | ||
| refreshTokenRepository.deleteAllByExpiresAtBefore(LocalDateTime.now()); | ||
| private String extractProfileImageUrl(SupabaseUserResponse supabaseUser) { | ||
| Map<String, Object> userMetadata = supabaseUser.getUserMetadata(); | ||
| if (userMetadata == null) { | ||
| return null; | ||
| } | ||
| Object avatarUrl = userMetadata.get("avatar_url"); | ||
| if (avatarUrl instanceof String avatarStr && !avatarStr.isBlank()) { | ||
| return avatarStr; | ||
| } | ||
| Object pictureUrl = userMetadata.get("picture"); | ||
| if (pictureUrl instanceof String pictureStr && !pictureStr.isBlank()) { | ||
| return pictureStr; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private String buildUniqueNickname(SupabaseUserResponse supabaseUser) { | ||
| String base = "user"; | ||
| Map<String, Object> userMetadata = supabaseUser.getUserMetadata(); | ||
| if (userMetadata != null) { | ||
| base = | ||
| firstNonBlank( | ||
| userMetadata, "nickname", "name", "full_name", "preferred_username", "user_name"); | ||
| } | ||
|
|
||
| if (base == null || base.isBlank()) { | ||
| base = "user"; | ||
| } | ||
|
|
||
| String sanitized = base.replaceAll("\\s+", ""); | ||
| sanitized = sanitized.replaceAll("[^a-zA-Z0-9._-]", ""); | ||
| if (sanitized.isBlank()) { | ||
| sanitized = "user"; | ||
| } | ||
|
|
||
| String candidate = sanitized; | ||
| int attempts = 0; | ||
| while (userRepository.existsByNickname(candidate) && attempts < 5) { | ||
| candidate = sanitized + randomSuffix(); | ||
| attempts++; | ||
| } | ||
| if (userRepository.existsByNickname(candidate)) { | ||
| candidate = "user" + randomSuffix(); | ||
| } | ||
| return candidate; | ||
| } | ||
|
|
||
| private String firstNonBlank(Map<String, Object> metadata, String... keys) { | ||
| for (String key : keys) { | ||
| Object value = metadata.get(key); | ||
| if (value instanceof String str && !str.isBlank()) { | ||
| return str; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private String randomSuffix() { | ||
| return UUID.randomUUID().toString().replace("-", "").substring(0, 6); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| package com.example.cp_main_be.global.supabase; | ||
|
|
||
| import com.example.cp_main_be.global.common.CustomApiException; | ||
| import com.example.cp_main_be.global.common.ErrorCode; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.http.HttpStatusCode; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.reactive.function.client.WebClient; | ||
| import reactor.core.publisher.Mono; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class SupabaseAuthClient { | ||
|
|
||
| private final WebClient webClient; | ||
|
|
||
| @Value("${supabase.url}") | ||
| private String supabaseUrl; | ||
|
|
||
| @Value("${supabase.service-key}") | ||
| private String supabaseServiceKey; | ||
|
|
||
| public SupabaseUserResponse fetchUser(String accessToken) { | ||
| if (accessToken == null || accessToken.isBlank()) { | ||
| throw new CustomApiException(ErrorCode.INVALID_REQUEST); | ||
| } | ||
|
|
||
| return webClient | ||
| .get() | ||
| .uri(supabaseUrl + "/auth/v1/user") | ||
| .header("apikey", supabaseServiceKey) | ||
| .header("Authorization", "Bearer " + accessToken) | ||
| .retrieve() | ||
| .onStatus( | ||
| status -> status.value() == 401 || status.value() == 403, | ||
| response -> Mono.error(new CustomApiException(ErrorCode.INVALID_TOKEN))) | ||
| .onStatus( | ||
| HttpStatusCode::isError, | ||
| response -> Mono.error(new CustomApiException(ErrorCode.INVALID_REQUEST))) | ||
| .bodyToMono(SupabaseUserResponse.class) | ||
| .block(); | ||
|
Comment on lines
+24
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "SupabaseAuthClient.java" | head -5Repository: Const-Project/MainBE Length of output: 142 🏁 Script executed: fd "SupabaseAuthClient.java" --type fRepository: Const-Project/MainBE Length of output: 140 🏁 Script executed: rg "SupabaseAuthClient" --type java -lRepository: Const-Project/MainBE Length of output: 221 🏁 Script executed: cat -n src/main/java/com/example/cp_main_be/global/supabase/SupabaseAuthClient.javaRepository: Const-Project/MainBE Length of output: 1861 🏁 Script executed: head -50 src/main/java/com/example/cp_main_be/global/supabase/SupabaseAuthClient.javaRepository: Const-Project/MainBE Length of output: 1553 🏁 Script executed: rg "\.block\(" src/main/java --type java -B 3 -A 1Repository: Const-Project/MainBE Length of output: 1951 🏁 Script executed: rg "Duration\." src/main/java --type java | head -20Repository: Const-Project/MainBE Length of output: 1162 🏁 Script executed: rg "timeout\|TimeoutException" src/main/java --type java -i | head -20Repository: Const-Project/MainBE Length of output: 46 WebClient.block() 타임아웃 누락으로 인한 스레드 고갈 위험 Line 42의 제안 변경+import java.time.Duration;
import com.example.cp_main_be.global.common.CustomApiException;
import com.example.cp_main_be.global.common.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono; .bodyToMono(SupabaseUserResponse.class)
- .block();
+ .block(Duration.ofSeconds(5));🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.example.cp_main_be.global.supabase; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import java.util.Map; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| public class SupabaseUserResponse { | ||
|
|
||
| private String id; | ||
| private String email; | ||
|
|
||
| @JsonProperty("app_metadata") | ||
| private Map<String, Object> appMetadata; | ||
|
|
||
| @JsonProperty("user_metadata") | ||
| private Map<String, Object> userMetadata; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ALTER TABLE users | ||
| ADD COLUMN IF NOT EXISTS oauth_provider varchar(50), | ||
| ADD COLUMN IF NOT EXISTS oauth_subject varchar(255); | ||
|
Comment on lines
+1
to
+3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OAuth 사용자 중복 방지를 위한 유니크 제약 필요 Line 1-3에 컬럼은 추가되었지만 🛠️ 제안 변경 ALTER TABLE users
ADD COLUMN IF NOT EXISTS oauth_provider varchar(50),
- ADD COLUMN IF NOT EXISTS oauth_subject varchar(255);
+ ADD COLUMN IF NOT EXISTS oauth_subject varchar(255),
+ ADD CONSTRAINT uq_users_oauth UNIQUE (oauth_provider, oauth_subject);🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Const-Project/MainBE
Length of output: 316
🏁 Script executed:
Repository: Const-Project/MainBE
Length of output: 1666
SupabaseLoginRequest에 검증 애노테이션과@Valid추가 필요SupabaseLoginRequest에 검증 애노테이션이 없어서 빈accessToken이 서비스로 직접 전달될 수 있습니다. 불필요한 Supabase 호출과 오류를 방지하려면 다음 두 가지를 수정하세요:@NotBlank(message = "accessToken은 필수입니다")@Valid추가✅ 수정 제안
SupabaseLoginRequest.java:
AuthController.java:
🤖 Prompt for AI Agents