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
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
@@ -1,6 +1,7 @@
package com.example.cp_main_be.domain.member.auth.presentation;

import com.example.cp_main_be.domain.member.auth.dto.request.RegistrationRequest;
import com.example.cp_main_be.domain.member.auth.dto.request.SupabaseLoginRequest;
import com.example.cp_main_be.domain.member.auth.dto.response.AnonymousRegistrationResponse;
import com.example.cp_main_be.domain.member.auth.dto.response.TokenRefreshResponse;
import com.example.cp_main_be.domain.member.auth.service.AuthService;
Expand Down Expand Up @@ -39,4 +40,15 @@ public ResponseEntity<ApiResponse<AnonymousRegistrationResponse>> signup(
AnonymousRegistrationResponse response = authService.registerNewUser(request, deviceId);
return ResponseEntity.ok(ApiResponse.success(response));
}

@Operation(summary = "Supabase 소셜 로그인", description = "Supabase OAuth 토큰을 우리 서비스 토큰으로 교환합니다.")
@PostMapping("/supabase")
public ResponseEntity<ApiResponse<AnonymousRegistrationResponse>> loginWithSupabase(
@RequestBody SupabaseLoginRequest request,
@RequestHeader(value = "X-Client-Device-Id", required = false) String deviceId) {
Comment on lines +44 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find and display SupabaseLoginRequest
echo "=== SupabaseLoginRequest.java ===" 
file=$(fd -a 'SupabaseLoginRequest.java' 2>/dev/null | head -n 1)
if [ -n "$file" ]; then
  cat -n "$file"
else
  echo "File not found"
fi

Repository: Const-Project/MainBE

Length of output: 316


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find and display AuthController around the loginWithSupabase method
echo "=== AuthController.java (lines 40-55) ===" 
file=$(fd -a 'AuthController.java' 2>/dev/null | head -n 1)
if [ -n "$file" ]; then
  sed -n '40,55p' "$file" | cat -n
  echo ""
  echo "=== Checking imports ===" 
  head -n 30 "$file" | grep -i "import"
else
  echo "File not found"
fi

Repository: Const-Project/MainBE

Length of output: 1666


SupabaseLoginRequest에 검증 애노테이션과 @Valid 추가 필요

SupabaseLoginRequest에 검증 애노테이션이 없어서 빈 accessToken이 서비스로 직접 전달될 수 있습니다. 불필요한 Supabase 호출과 오류를 방지하려면 다음 두 가지를 수정하세요:

  1. DTO에 검증 애노테이션 추가: @NotBlank(message = "accessToken은 필수입니다")
  2. 컨트롤러 메서드에 @Valid 추가
✅ 수정 제안

SupabaseLoginRequest.java:

 package com.example.cp_main_be.domain.member.auth.dto.request;

 import lombok.Getter;
+import jakarta.validation.constraints.NotBlank;

 `@Getter`
 public class SupabaseLoginRequest {
+  `@NotBlank`(message = "accessToken은 필수입니다")
   private String accessToken;
 }

AuthController.java:

 public ResponseEntity<ApiResponse<AnonymousRegistrationResponse>> loginWithSupabase(
-    `@RequestBody` SupabaseLoginRequest request,
+    `@RequestBody` `@Valid` SupabaseLoginRequest request,
     `@RequestHeader`(value = "X-Client-Device-Id", required = false) String deviceId) {
🤖 Prompt for AI Agents
In
`@src/main/java/com/example/cp_main_be/domain/member/auth/presentation/AuthController.java`
around lines 44 - 48, Add validation to the SupabaseLoginRequest DTO by
annotating the accessToken field with `@NotBlank`(message = "accessToken은 필수입니다")
(ensure you use the javax/jakarta.validation import), and update the
AuthController.loginWithSupabase method signature to accept a validated request
by adding `@Valid` to the `@RequestBody` parameter (i.e., change to `@Valid`
`@RequestBody` SupabaseLoginRequest request); this enforces non-empty accessToken
and prevents invalid requests from reaching the service.


AnonymousRegistrationResponse response =
authService.loginWithSupabase(request.getAccessToken(), deviceId);
return ResponseEntity.ok(ApiResponse.success(response));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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
fi

Repository: 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"
fi

Repository: 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 -50

Repository: Const-Project/MainBE

Length of output: 2725


외부 Supabase 호출을 트랜잭션 밖으로 분리하세요

loginWithSupabase() 메서드에서 @Transactional 내부에서 supabaseAuthClient.fetchUser(accessToken)로 외부 HTTP 호출(WebClient)을 수행하고 있습니다. 이로 인해 DB 트랜잭션이 네트워크 요청 완료까지 열린 상태로 유지되어 커넥션 풀 고갈과 성능 저하를 초래할 수 있습니다. Supabase 호출을 별도 메서드로 분리하고 DB 작업만 트랜잭션으로 묶으시기 바랍니다.

✅ 분리 예시
-  `@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
In
`@src/main/java/com/example/cp_main_be/domain/member/auth/service/AuthService.java`
around lines 99 - 102, loginWithSupabase currently performs the external HTTP
call supabaseAuthClient.fetchUser(accessToken) inside a method annotated with
`@Transactional`, which keeps the DB transaction open during network I/O; extract
the Supabase call into a separate non-transactional helper (e.g.,
fetchSupabaseUser(...) or make fetchUser call before entering the transactional
scope) so that loginWithSupabase only performs database work inside the
transactional block; keep `@Transactional` on the DB-updating portion (the method
doing member creation/updates) and call the external fetchUser outside that
transaction to prevent connection pool exhaustion.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find User entity file
fd -a 'User\.java' --type f | head -5

Repository: Const-Project/MainBE

Length of output: 155


🏁 Script executed:

# Check User entity definition
find . -name 'User.java' -type f | head -1 | xargs wc -l

Repository: Const-Project/MainBE

Length of output: 142


🏁 Script executed:

# Find migration files
fd -a 'V.*\.sql' --type f | head -20

Repository: 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 -30

Repository: 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.java

Repository: Const-Project/MainBE

Length of output: 6225


🏁 Script executed:

cat /home/jailuser/git/src/main/resources/db/migration/V4__add_user_oauth_fields.sql

Repository: 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 1

Repository: 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 15

Repository: 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 -lh

Repository: 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 25

Repository: 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 15

Repository: 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 10

Repository: Const-Project/MainBE

Length of output: 46


🏁 Script executed:

# Check all files in resources
find src/main/resources -type f | head -20

Repository: Const-Project/MainBE

Length of output: 327


🏁 Script executed:

# Look for schema files or flyway initialization
fd '(schema|initial|ddl)' src/main/resources/ -i

Repository: 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/null

Repository: 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.java

Repository: 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 -50

Repository: 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 20

Repository: Const-Project/MainBE

Length of output: 46


🏁 Script executed:

# Check new-schema.sql as well
grep -n "CREATE TABLE.*users" new-schema.sql -A 20

Repository: 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 -80

Repository: 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.java

Repository: Const-Project/MainBE

Length of output: 46


🏁 Script executed:

# Read entity-schema.sql directly
head -50 entity-schema.sql

Repository: 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 -30

Repository: Const-Project/MainBE

Length of output: 46


🏁 Script executed:

# Check new-schema.sql
head -50 new-schema.sql

Repository: 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 2

Repository: 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 2

Repository: 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 10

Repository: 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 30

Repository: 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.java

Repository: 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.java

Repository: 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.java

Repository: 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 5

Repository: 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/ -i

Repository: 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 v1

Repository: 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}")
EOF

Repository: 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 필드에는 길이 제약이 없으며, buildUniqueNickname() 메서드가 사용자 메타데이터에서 매우 긴 문자열을 받을 경우 sanitize 후 6자 suffix 추가로 인해 varchar(255) 제한을 초과할 수 있습니다. 또한 Supabase에서 email이 null일 수 있는데 현재 코드는 이를 검증하지 않습니다. nickname을 최대 250자로 제한하고, email이 null인 경우 기본값 처리 또는 검증을 추가해주세요.

🤖 Prompt for AI Agents
In
`@src/main/java/com/example/cp_main_be/domain/member/auth/service/AuthService.java`
around lines 124 - 131, buildUniqueNickname() can produce nicknames exceeding
the DB varchar(255) and doesn't handle null Supabase emails; modify
buildUniqueNickname() to sanitize input, truncate the base nickname to 250
characters before appending the 6-char suffix (ensuring final length ≤ 256 or
better truncate to 249+6 to be safe for 255), and enforce a min-safe fallback
when metadata is empty. Also update the code that reads supabaseUser.getEmail()
(used in User.builder()) to validate null emails and either supply a safe
default (e.g., "no-email@local") or throw a clear validation exception before
constructing the User; reference the buildUniqueNickname() method and the
User.builder() usage so the nickname and email constraints are applied
consistently.

.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 =
Expand All @@ -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();
Expand All @@ -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
Expand Up @@ -53,6 +53,12 @@ public void updateLastVisitedGarden(Long gardenId) {

private String profileImageUrl;

@Column(name = "oauth_provider")
private String oauthProvider;

@Column(name = "oauth_subject")
private String oauthSubject;

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<Avatar> avatarList = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByIdWithGardensAndAvatars(@Param("userId") Long userId);

Boolean existsByNickname(String nickname);

Optional<User> findByOauthProviderAndOauthSubject(String oauthProvider, String oauthSubject);
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
authorize
.requestMatchers(
"/api/v1/auth/signup",
"/api/v1/auth/supabase",
"/api/v1/auth/refresh",
"/api/v1/policy",
"/swagger-ui/**", // Swagger UI 페이지
Expand Down
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "SupabaseAuthClient.java" | head -5

Repository: Const-Project/MainBE

Length of output: 142


🏁 Script executed:

fd "SupabaseAuthClient.java" --type f

Repository: Const-Project/MainBE

Length of output: 140


🏁 Script executed:

rg "SupabaseAuthClient" --type java -l

Repository: Const-Project/MainBE

Length of output: 221


🏁 Script executed:

cat -n src/main/java/com/example/cp_main_be/global/supabase/SupabaseAuthClient.java

Repository: Const-Project/MainBE

Length of output: 1861


🏁 Script executed:

head -50 src/main/java/com/example/cp_main_be/global/supabase/SupabaseAuthClient.java

Repository: Const-Project/MainBE

Length of output: 1553


🏁 Script executed:

rg "\.block\(" src/main/java --type java -B 3 -A 1

Repository: Const-Project/MainBE

Length of output: 1951


🏁 Script executed:

rg "Duration\." src/main/java --type java | head -20

Repository: Const-Project/MainBE

Length of output: 1162


🏁 Script executed:

rg "timeout\|TimeoutException" src/main/java --type java -i | head -20

Repository: Const-Project/MainBE

Length of output: 46


WebClient.block() 타임아웃 누락으로 인한 스레드 고갈 위험

Line 42의 .block()에 타임아웃이 설정되어 있지 않아 Supabase 응답 지연 또는 장애 시 요청 스레드가 무한정 대기하며 고갈될 수 있습니다. Duration.ofSeconds(5)를 추가하고 필요한 import를 포함하세요.

제안 변경
+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
In `@src/main/java/com/example/cp_main_be/global/supabase/SupabaseAuthClient.java`
around lines 24 - 42, The fetchUser method in SupabaseAuthClient uses
WebClient.block() with no timeout; update the call in fetchUser(String
accessToken) to use a bounded wait such as .block(Duration.ofSeconds(5)) and add
the required import (java.time.Duration) to prevent thread starvation when
Supabase is slow or unresponsive; keep existing onStatus handlers and return
type unchanged.

}
}
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;
}
3 changes: 3 additions & 0 deletions src/main/resources/db/migration/V4__add_user_oauth_fields.sql
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

OAuth 사용자 중복 방지를 위한 유니크 제약 필요

Line 1-3에 컬럼은 추가되었지만 (oauth_provider, oauth_subject)에 대한 유니크 제약이 없어 동일 OAuth 계정이 중복 생성될 수 있습니다. 로그인 매핑이 모호해질 수 있으므로 제약을 권장합니다.

🛠️ 제안 변경
 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
In `@src/main/resources/db/migration/V4__add_user_oauth_fields.sql` around lines 1
- 3, The migration adds oauth_provider and oauth_subject to users but lacks a
uniqueness constraint, allowing duplicate OAuth accounts; update the migration
to add a unique constraint on the (oauth_provider, oauth_subject) pair for the
users table (e.g., via ALTER TABLE users ADD CONSTRAINT ... UNIQUE on
oauth_provider and oauth_subject) so that duplicate OAuth mappings cannot be
created and lookups in authentication logic that rely on this pair remain
deterministic.