feat(auth): 네이버 로그인 Supabase Admin 브리지 구현 - #59
Conversation
네이버는 Supabase가 기본 지원하지 않는 OAuth 제공자라, #45 마이그레이션 이후 /api/auth/naver/* 가 naver_not_implemented 스텁으로 비활성 상태였다. - 네이버 OAuth 헬퍼(getNaverAuthUrl/getNaverToken/getNaverUser) 복원 (자체 JWT 의존 제거, fetch-with-timeout 기반) - naver/login: authorize 리다이렉트 + state 쿠키 복원 - naver/callback: 네이버 프로필 수신 → Supabase Admin(service_role)으로 유저 생성/조회 → magiclink 토큰을 verifyOtp로 교환해 일반 Supabase 세션 쿠키 발급. 카카오/구글/애플과 동일하게 getUser() 세션으로 일원화. - 이메일 미제공 시 결정적 placeholder로 동일 사용자 식별, 닉네임 unique 충돌 시 naverId 기반으로 대체. Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 12 minutes and 4 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
WalkthroughNaver OAuth 라이브러리와 로그인/콜백 라우트를 추가해 state 쿠키 기반 인증 흐름을 구현하고, Supabase Admin으로 프로필 조회/생성 및 magiclink로 세션을 발급하며 profiles.naver_id 컬럼을 마이그레이션합니다. ChangesNaver OAuth 인증 흐름
Sequence Diagram(s)sequenceDiagram
participant Browser as 사용자 브라우저
participant LoginRoute as /api/auth/naver/login
participant NaverAuth as Naver 인가 서버
participant CallbackRoute as /api/auth/naver/callback
participant NaverAPI as Naver API (토큰/프로필)
participant SupabaseAdmin as Supabase Admin
participant SupabaseAuth as Supabase Auth (verifyOtp)
Browser->>LoginRoute: GET /api/auth/naver/login
LoginRoute->>Browser: Set-Cookie(state) + Redirect(네이버 인가 URL)
Browser->>NaverAuth: 사용자 인증/동의
NaverAuth->>Browser: Redirect 백엔드 콜백?code&state
Browser->>CallbackRoute: GET /api/auth/naver/callback?code&state
CallbackRoute->>NaverAPI: getNaverToken(code,state)
NaverAPI-->>CallbackRoute: access_token
CallbackRoute->>NaverAPI: getNaverUser(access_token)
NaverAPI-->>CallbackRoute: user profile
CallbackRoute->>SupabaseAdmin: 프로필 조회/생성 (naver_id/email 기준)
SupabaseAdmin-->>CallbackRoute: 프로필/사용자 생성 결과
CallbackRoute->>SupabaseAuth: generate magiclink -> verifyOtp(hashed_token)
SupabaseAuth-->>CallbackRoute: 세션 확정
CallbackRoute->>Browser: Redirect(final) + Set-Cookie(last_login_provider)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45분 Possibly related issues
Suggested labels
리뷰 주의사항아래 항목들은 반드시 수정/테스트 포함되어야 함. 이유(Why) + 해결안(How) + 코드 스니펫(권장 적용 위치)을 제시한다.
// callback/route.ts (검증부)
const stateFromQuery = req.nextUrl.searchParams.get('state');
const stateFromCookie = req.cookies.get(STATE_COOKIE)?.value;
if (!stateFromQuery || !stateFromCookie || stateFromQuery !== stateFromCookie) {
return redirectWithStateCleanup('/login?error=naver_login_failed', req);
}
const error = req.nextUrl.searchParams.get('error');
if (error) {
console.warn(`Naver OAuth error: ${error}`);
return redirectWithStateCleanup(`/login?error=naver_oauth_${encodeURIComponent(error)}`, req);
}
const getPlaceholderEmail = (naverId: string) => `naver+${naverId}`@placeholder.local``;
async function ensureUniqueNickname(base: string) {
for (let i = 0; i < 5; i++) {
const cand = i === 0 ? base : `${base}_naver${crypto.randomUUID().slice(0,4)}`;
const { data } = await adminClient.from('profiles').select('id').eq('nickname', cand).maybeSingle();
if (!data) return cand;
}
return `naver_${Date.now()}`;
}
if (!magicLinkData?.properties?.hashed_token) throw new Error('missing magiclink token');
try {
await supabaseAuth.auth.verifyOtp({ email: email, token: magicLinkData.properties.hashed_token, type: 'magiclink' });
} catch (e) {
// 재시도 또는 실패 분기
}
타협 없음: 위 7가지는 머지 전 반드시 코드·테스트·마이그레이션 문서로 보완할 것. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/api/auth/naver/callback/route.ts`:
- Around line 56-66: The current lookup uses only profiles.email (authEmail)
which can split the same Naver account; change the lookup logic in the callback
flow (around extractNaverUserInfo/resolveEmail/createAdminClient usage) to first
query profiles by the stable naverId (user_metadata.naver_id) and only fall back
to profiles.email (authEmail) for migration/backfill; if the profiles table has
no naver_id column, create or use an identity mapping table to map naverId →
profile_id and query that first, then update the profile with email/backfill as
needed.
In `@src/lib/auth/naver.ts`:
- Around line 143-145: The nickname fallback currently uses nullish coalescing
on naverUser.nickname which treats an empty string as a valid value and allows
empty nicknames to persist; change the nickname assignment (the line setting
const nickname using naverUser.nickname, naverUser.name and naverId) to
explicitly treat empty/whitespace-only strings as missing by trimming and
checking truthiness, then fall back to naverUser.name (also trimmed/checked) and
finally to the generated `naver_${naverId}` default.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: dd5042a1-9675-4468-a11e-2c9c4dbbe714
📒 Files selected for processing (3)
src/app/api/auth/naver/callback/route.tssrc/app/api/auth/naver/login/route.tssrc/lib/auth/naver.ts
- naver_id를 profiles 1차 식별자로 사용 (이메일 단독 조회 시 같은 네이버 계정이 갈라지는 문제 해결). profiles.naver_id 컬럼+unique 인덱스 추가하고 handle_new_user 트리거가 user_metadata.naver_id를 채우도록 갱신. - 콜백: naver_id로 먼저 조회, 미존재 시 이메일로 매칭 후 naver_id 백필. - extractNaverUserInfo: 빈 문자열/공백 닉네임·이메일을 "값 없음"으로 취급해 name → naver_<id> fallback이 정상 동작하도록 수정. Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@coderabbitai 리뷰 반영했습니다 (3334d46).
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check the migration file and updated source files
echo "=== Migration file ==="
fd "20260611000000_profiles_naver_id" --exec cat {}
echo ""
echo "=== naver.ts - extractNaverUserInfo ==="
fd "naver.ts" --search-path src/lib --exec cat {}
echo ""
echo "=== callback route ==="
fd "route.ts" --search-path src/app/api/auth/naver/callback --exec cat {}
echo ""
echo "=== handle_new_user trigger (any SQL migration files) ==="
fd "handle_new_user" --exec cat {}
echo ""
echo "=== All migration files ==="
fd "\.sql$" --exec ls -la {}Length of output: 12139 반영 확인했습니다. 전체적으로 핵심 피드백이 잘 구현됐고, 두 가지만 추가로 짚겠습니다. 1. 백필 update —
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/api/auth/naver/callback/route.ts`:
- Around line 75-87: The current email-fallback backfill unconditionally updates
profiles.naver_id and can overwrite an existing naver linkage; change the logic
in the callback handler that queries profiles (the admin.from("profiles") call
producing byEmail) to also select the naver_id field, then only perform the
update (the admin.from("profiles").update... using naverId that produces
backfillError) if byEmail.naver_id is null/empty/undefined; otherwise skip the
update and do not throw—use the existing variables byEmail, backfillError,
naverId and authEmail to implement this guard.
In `@supabase/migrations/20260611000000_profiles_naver_id.sql`:
- Around line 15-17: Normalize empty/whitespace naver_id to NULL at write and
make the partial unique index use the same normalization: in the TS callback
that writes profiles.naver_id (src/app/api/auth/naver/callback/route.ts around
the code that reads new.raw_user_meta_data->>'naver_id') trim and convert empty
strings to null before saving (e.g., apply btrim/trim and treat '' as null), and
in the migration change the index definition (profiles_naver_id_key) to use the
same expression in its predicate (e.g., WHERE nullif(btrim(naver_id), '') IS NOT
NULL or create the index on the normalized expression) so both insert/update
logic and the partial-unique index use identical nullif(btrim(...), '')
normalization.
- Around line 20-56: The security-definer function public.handle_new_user lacks
a fixed search_path; update its declaration to include "SET search_path =
public" so name resolution is locked to the public schema (i.e., change the
function header/footer options for public.handle_new_user to LANGUAGE plpgsql
SECURITY DEFINER SET search_path = public) to prevent external search_path
influencing object/function lookup when the after insert on auth.users trigger
runs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: acec9617-8234-4fd9-8c32-861f9d46d594
📒 Files selected for processing (3)
src/app/api/auth/naver/callback/route.tssrc/lib/auth/naver.tssupabase/migrations/20260611000000_profiles_naver_id.sql
- 콜백: 이메일 매칭 백필 시 byEmail.naver_id가 비어있을 때만 갱신해 기존 네이버 연결을 덮어쓰지 않도록 가드. - 마이그레이션: naver_id 빈 문자열/공백을 NULL로 정규화(트리거 nullif(btrim(...))), unique 인덱스 predicate도 동일 정규화로 일치. - handle_new_user에 SET search_path = public 추가 (SECURITY DEFINER 함수의 search_path 고정). Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@coderabbitai 2차 리뷰 3건 반영했습니다 (382e5f2).
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== callback route.ts ==="
fd "route.ts" --search-path src/app/api/auth/naver/callback --exec cat {}
echo ""
echo "=== migration file ==="
fd "20260611000000_profiles_naver_id" --exec cat {}Length of output: 7649 3건 모두 코드에서 확인했습니다. ✅ 반영 확인
|
|
@coderabbitai full review |
✅ Action performedFull review finished. |
|
@coderabbitai 별도 이슈 대신 이 PR에서 바로 수정했습니다 (f726ef0). 동시요청 race로 |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== callback route.ts (current) ==="
fd "route.ts" --search-path src/app/api/auth/naver/callback --exec cat {}Length of output: 6471 확인했습니다. race 처리 블록이 정확히 구현됐습니다. // createError → "already exists" 분기
const { data: raced } = await admin
.from("profiles")
.select("phone_number, terms_agreed_at")
.eq("naver_id", naverId)
.maybeSingle();
const racedProfile =
raced ??
(await admin.from("profiles").select(...).eq("email", authEmail).maybeSingle()).data;
isNewUser = false;
profileComplete = !!racedProfile?.phone_number && !!racedProfile?.terms_agreed_at;
이전 리뷰에서 제기한 모든 항목이 반영됐습니다. PR approve 진행합니다. [approve] |
|
good |
* refactor: 코드레빗 리뷰 반영 — 터치 드래그 스크롤 방지 개선 및 E2E 안정화 * test(e2e): 호스트 플로우 내 고정 대기 시간 제거 및 요소 가시성 검사 보강 * feat(auth): 네이버 로그인 Supabase Admin 브리지 구현 (#59) * feat(auth): 네이버 로그인 Supabase Admin 브리지 구현 네이버는 Supabase가 기본 지원하지 않는 OAuth 제공자라, #45 마이그레이션 이후 /api/auth/naver/* 가 naver_not_implemented 스텁으로 비활성 상태였다. - 네이버 OAuth 헬퍼(getNaverAuthUrl/getNaverToken/getNaverUser) 복원 (자체 JWT 의존 제거, fetch-with-timeout 기반) - naver/login: authorize 리다이렉트 + state 쿠키 복원 - naver/callback: 네이버 프로필 수신 → Supabase Admin(service_role)으로 유저 생성/조회 → magiclink 토큰을 verifyOtp로 교환해 일반 Supabase 세션 쿠키 발급. 카카오/구글/애플과 동일하게 getUser() 세션으로 일원화. - 이메일 미제공 시 결정적 placeholder로 동일 사용자 식별, 닉네임 unique 충돌 시 naverId 기반으로 대체. Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(auth): 코드래빗 리뷰 반영 — 네이버 식별자/닉네임 보강 - naver_id를 profiles 1차 식별자로 사용 (이메일 단독 조회 시 같은 네이버 계정이 갈라지는 문제 해결). profiles.naver_id 컬럼+unique 인덱스 추가하고 handle_new_user 트리거가 user_metadata.naver_id를 채우도록 갱신. - 콜백: naver_id로 먼저 조회, 미존재 시 이메일로 매칭 후 naver_id 백필. - extractNaverUserInfo: 빈 문자열/공백 닉네임·이메일을 "값 없음"으로 취급해 name → naver_<id> fallback이 정상 동작하도록 수정. Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(auth): 코드래빗 2차 리뷰 반영 — naver_id 백필 가드/정규화/보안 - 콜백: 이메일 매칭 백필 시 byEmail.naver_id가 비어있을 때만 갱신해 기존 네이버 연결을 덮어쓰지 않도록 가드. - 마이그레이션: naver_id 빈 문자열/공백을 NULL로 정규화(트리거 nullif(btrim(...))), unique 인덱스 predicate도 동일 정규화로 일치. - handle_new_user에 SET search_path = public 추가 (SECURITY DEFINER 함수의 search_path 고정). Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(auth): 네이버 동시요청 race 시 기존 사용자 추가정보 오라우팅 수정 createUser가 "already exists"로 실패하는 동시요청 race에서는 기존 사용자인데도 profileComplete를 false로 고정해, 추가정보 입력 페이지로 잘못 보내던 문제를 수정. race 감지 시 naver_id(없으면 이메일)로 실제 프로필 상태를 재조회해 isNewUser=false + 정확한 profileComplete로 분기. Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: kokkumong <102169091+kokkumong@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(auth): 네이버 로그인 Supabase Admin 브리지 구현 (#59) * feat(auth): 네이버 로그인 Supabase Admin 브리지 구현 네이버는 Supabase가 기본 지원하지 않는 OAuth 제공자라, #45 마이그레이션 이후 /api/auth/naver/* 가 naver_not_implemented 스텁으로 비활성 상태였다. - 네이버 OAuth 헬퍼(getNaverAuthUrl/getNaverToken/getNaverUser) 복원 (자체 JWT 의존 제거, fetch-with-timeout 기반) - naver/login: authorize 리다이렉트 + state 쿠키 복원 - naver/callback: 네이버 프로필 수신 → Supabase Admin(service_role)으로 유저 생성/조회 → magiclink 토큰을 verifyOtp로 교환해 일반 Supabase 세션 쿠키 발급. 카카오/구글/애플과 동일하게 getUser() 세션으로 일원화. - 이메일 미제공 시 결정적 placeholder로 동일 사용자 식별, 닉네임 unique 충돌 시 naverId 기반으로 대체. Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(auth): 코드래빗 리뷰 반영 — 네이버 식별자/닉네임 보강 - naver_id를 profiles 1차 식별자로 사용 (이메일 단독 조회 시 같은 네이버 계정이 갈라지는 문제 해결). profiles.naver_id 컬럼+unique 인덱스 추가하고 handle_new_user 트리거가 user_metadata.naver_id를 채우도록 갱신. - 콜백: naver_id로 먼저 조회, 미존재 시 이메일로 매칭 후 naver_id 백필. - extractNaverUserInfo: 빈 문자열/공백 닉네임·이메일을 "값 없음"으로 취급해 name → naver_<id> fallback이 정상 동작하도록 수정. Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(auth): 코드래빗 2차 리뷰 반영 — naver_id 백필 가드/정규화/보안 - 콜백: 이메일 매칭 백필 시 byEmail.naver_id가 비어있을 때만 갱신해 기존 네이버 연결을 덮어쓰지 않도록 가드. - 마이그레이션: naver_id 빈 문자열/공백을 NULL로 정규화(트리거 nullif(btrim(...))), unique 인덱스 predicate도 동일 정규화로 일치. - handle_new_user에 SET search_path = public 추가 (SECURITY DEFINER 함수의 search_path 고정). Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(auth): 네이버 동시요청 race 시 기존 사용자 추가정보 오라우팅 수정 createUser가 "already exists"로 실패하는 동시요청 race에서는 기존 사용자인데도 profileComplete를 false로 고정해, 추가정보 입력 페이지로 잘못 보내던 문제를 수정. race 감지 시 naver_id(없으면 이메일)로 실제 프로필 상태를 재조회해 isNewUser=false + 정확한 profileComplete로 분기. Refs #46 Part of #58 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * refactor(auth): 네이버 로그인 및 E2E 테스트 관련 코드레빗 리뷰 반영 * refactor: 캘린더 어댑터 상속 구조화 및 에러 핸들링 계층 구조 구축 * chore: PR #61 코드레빗 리뷰 피드백 반영 --------- Co-authored-by: kokkumong <102169091+kokkumong@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
🚀 작업 내용 (What)
naver_not_implemented스텁이던 것을 활성화).src/lib/auth/naver.ts: 네이버 OAuth 헬퍼(authorize URL/토큰/프로필) 복원 — 자체 JWT 의존 제거,fetch-with-timeout기반.naver/login: authorize 리다이렉트 + state 쿠키.naver/callback: 네이버 프로필 수신 → Supabase Admin(service_role)으로 유저 생성/조회 → magiclink 토큰을verifyOtp로 교환해 일반 Supabase 세션 쿠키 발급.profiles.naver_id(컬럼+트리거)로 안정적 식별, 빈 닉네임 fallback 보강.📣 핵심 변경 이유 (Why)
signInWithOAuth)과 같은 네이티브 흐름을 쓸 수 없음.supabase.auth.getUser()세션으로 일원화.📸 스크린샷 (Visuals, 선택)
⚙️ 배포 전 필요한 설정 (Deploy notes)
NAVER_CLIENT_ID,NAVER_CLIENT_SECRET,NAVER_REDIRECT_URI=https://moim-app-eosin.vercel.app/api/auth/naver/callback,SUPABASE_SERVICE_ROLE_KEYhttps://moim-app-eosin.vercel.app/api/auth/naver/callback(Supabase 콜백 아님)20260611000000_profiles_naver_id.sql적용 필요 (naver_id 컬럼/트리거)🔗 관련 이슈 (Issue)
Refs #46
Part of #58
🤖 Generated with Claude Code