[Phase 2] feat(auth): AuthView 로그인·회원가입 이메일 인증 3단계 API 연결 - #85
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAuthView.vue now uses a three-step signup flow with email verification, then nickname/password registration. The frontend also adds auth API helpers, a Pinia auth store with token persistence, and HTTP 401 refresh handling. ChangesAuth signup and token flow
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 `@frontend/src/views/AuthView.vue`:
- Around line 100-107: The verification code field in AuthView.vue currently
allows non-digit characters and incomplete values even though the API expects a
6-digit code. Update the input bound to verifyCode in the
registration/verification form so it only accepts digits and enforces exactly 6
characters before submission, using the existing input element with id reg-code
and the verifyCode model to keep the UI aligned with the expected format.
- Around line 247-250: The signup flow in AuthView.vue currently leaves users
stuck on Step 3 when `parseError` returns `EMAIL_NOT_VERIFIED`, so handle that
case explicitly in the `auth.signup` catch block. Use the existing `switchMode`
flow (and related signup state in `AuthView.vue`) to move the user back to a
step where they can re-enter or re-trigger verification, while still showing the
verification message. Keep the default `parseError(e, '회원가입에 실패했습니다.')` path for
all other signup errors.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 34ecc115-234f-47ea-b6fc-6108ef7f5b64
📒 Files selected for processing (1)
frontend/src/views/AuthView.vue
| <input | ||
| id="reg-code" | ||
| v-model="verifyCode" | ||
| type="text" | ||
| placeholder="홍길동" | ||
| autocomplete="name" | ||
| placeholder="6자리 코드 입력" | ||
| maxlength="6" | ||
| required | ||
| /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Constrain the verification code input to the expected 6-digit format.
The UI asks for a 6-digit code, but the input currently accepts non-digits and shorter values before calling the API.
Proposed fix
<input
id="reg-code"
v-model="verifyCode"
type="text"
+ inputmode="numeric"
+ autocomplete="one-time-code"
placeholder="6자리 코드 입력"
+ minlength="6"
maxlength="6"
+ pattern="[0-9]{6}"
required
/>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <input | |
| id="reg-code" | |
| v-model="verifyCode" | |
| type="text" | |
| placeholder="홍길동" | |
| autocomplete="name" | |
| placeholder="6자리 코드 입력" | |
| maxlength="6" | |
| required | |
| /> | |
| <input | |
| id="reg-code" | |
| v-model="verifyCode" | |
| type="text" | |
| inputmode="numeric" | |
| autocomplete="one-time-code" | |
| placeholder="6자리 코드 입력" | |
| minlength="6" | |
| maxlength="6" | |
| pattern="[0-9]{6}" | |
| required | |
| /> |
🤖 Prompt for 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.
In `@frontend/src/views/AuthView.vue` around lines 100 - 107, The verification
code field in AuthView.vue currently allows non-digit characters and incomplete
values even though the API expects a 6-digit code. Update the input bound to
verifyCode in the registration/verification form so it only accepts digits and
enforces exactly 6 characters before submission, using the existing input
element with id reg-code and the verifyCode model to keep the UI aligned with
the expected format.
| await auth.signup(email.value, password.value, nickname.value) | ||
| switchMode('login') | ||
| } catch (e) { | ||
| errorMsg.value = e?.response?.data?.message ?? '회원가입에 실패했습니다.' | ||
| errorMsg.value = parseError(e, '회원가입에 실패했습니다.') |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make EMAIL_NOT_VERIFIED recoverable from the signup step.
Line 250 can show “이메일 인증을 먼저 완료해주세요.” while the UI stays on Step 3, so the user has no obvious way to re-send or re-enter verification.
Proposed fix
} catch (e) {
+ if (e?.response?.data?.code === 'EMAIL_NOT_VERIFIED') {
+ registerStep.value = 'email'
+ verifyCode.value = ''
+ }
errorMsg.value = parseError(e, '회원가입에 실패했습니다.')
} finally {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await auth.signup(email.value, password.value, nickname.value) | |
| switchMode('login') | |
| } catch (e) { | |
| errorMsg.value = e?.response?.data?.message ?? '회원가입에 실패했습니다.' | |
| errorMsg.value = parseError(e, '회원가입에 실패했습니다.') | |
| await auth.signup(email.value, password.value, nickname.value) | |
| switchMode('login') | |
| } catch (e) { | |
| if (e?.response?.data?.code === 'EMAIL_NOT_VERIFIED') { | |
| registerStep.value = 'email' | |
| verifyCode.value = '' | |
| } | |
| errorMsg.value = parseError(e, '회원가입에 실패했습니다.') |
🤖 Prompt for 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.
In `@frontend/src/views/AuthView.vue` around lines 247 - 250, The signup flow in
AuthView.vue currently leaves users stuck on Step 3 when `parseError` returns
`EMAIL_NOT_VERIFIED`, so handle that case explicitly in the `auth.signup` catch
block. Use the existing `switchMode` flow (and related signup state in
`AuthView.vue`) to move the user back to a step where they can re-enter or
re-trigger verification, while still showing the verification message. Keep the
default `parseError(e, '회원가입에 실패했습니다.')` path for all other signup errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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 `@frontend/src/api/http.js`:
- Around line 88-90: The refresh-failure cleanup in the HTTP API flow only
removes the new storage keys, leaving the legacy accessToken fallback behind.
Update the error handling around the token refresh path in http.js to also clear
the old access token entry when redirecting to /login, keeping the cleanup
consistent with the accessToken lookup used earlier in the module.
- Around line 63-69: Queued requests in the http interceptor are replayed
without being marked as retried, so a second 401 can trigger another refresh
cycle. Update the retry path in the isRefreshing branch of http.js so the
replayed request carries the same _retry flag used elsewhere in the interceptor
before calling http(original), ensuring queued requests fail after one retry
instead of looping.
- Around line 76-78: The refresh request in the auth flow is bypassing the
shared client timeout because it uses axios.post directly. Update the refresh
logic in http.js so the refresh call goes through the existing http client or
explicitly applies the same 10s timeout, keeping the request bounded and
preventing isRefreshing from getting stuck in the refresh path.
In `@frontend/src/store/authStore.js`:
- Around line 24-29: The _saveTokens method in authStore is persisting the
refreshToken to localStorage, which should be removed. Keep only the accessToken
in JS-managed state/storage and update the refresh flow to use an
HttpOnly/SameSite cookie instead of exposing the refresh token in localStorage.
Ensure any callers of _saveTokens and related auth token handling still work
with the refresh token no longer being stored client-side.
- Around line 31-36: The logout cleanup in _clearTokens only removes the new
storage keys, but the HTTP client still reads the legacy accessToken fallback.
Update _clearTokens in authStore to also remove the old access-token key used by
http.js, alongside ACCESS_KEY and REFRESH_KEY, so stale tokens cannot survive
logout.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9abc9830-2564-4374-b1c2-552d13f23ca2
📒 Files selected for processing (3)
frontend/src/api/auth.jsfrontend/src/api/http.jsfrontend/src/store/authStore.js
✅ Files skipped from review due to trivial changes (1)
- frontend/src/api/auth.js
| if (isRefreshing) { | ||
| return new Promise((resolve, reject) => { | ||
| pendingQueue.push({ resolve, reject }) | ||
| }).then((token) => { | ||
| original.headers.Authorization = `Bearer ${token}` | ||
| return http(original) | ||
| }) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Mark queued requests as retried before replaying them.
Queued 401s are retried without _retry, so if the replay also returns 401 they can start another refresh cycle instead of failing once.
Proposed fix
if (isRefreshing) {
+ original._retry = true
return new Promise((resolve, reject) => {
pendingQueue.push({ resolve, reject })
}).then((token) => {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (isRefreshing) { | |
| return new Promise((resolve, reject) => { | |
| pendingQueue.push({ resolve, reject }) | |
| }).then((token) => { | |
| original.headers.Authorization = `Bearer ${token}` | |
| return http(original) | |
| }) | |
| if (isRefreshing) { | |
| original._retry = true | |
| return new Promise((resolve, reject) => { | |
| pendingQueue.push({ resolve, reject }) | |
| }).then((token) => { | |
| original.headers.Authorization = `Bearer ${token}` | |
| return http(original) | |
| }) |
🤖 Prompt for 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.
In `@frontend/src/api/http.js` around lines 63 - 69, Queued requests in the http
interceptor are replayed without being marked as retried, so a second 401 can
trigger another refresh cycle. Update the retry path in the isRefreshing branch
of http.js so the replayed request carries the same _retry flag used elsewhere
in the interceptor before calling http(original), ensuring queued requests fail
after one retry instead of looping.
| const res = await axios.post(`${baseURL}/api/v1/auth/refresh`, { | ||
| refreshToken: storedRefresh, | ||
| }) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether a global Axios timeout exists; if not, raw axios.post uses different timeout behavior than the configured http client.
rg -n -C2 'axios\.defaults\.timeout|timeout:\s*[0-9]+|axios\.create' frontendRepository: ssafy-salman/salmanhae
Length of output: 500
🏁 Script executed:
sed -n '1,180p' frontend/src/api/http.jsRepository: ssafy-salman/salmanhae
Length of output: 2595
🏁 Script executed:
sed -n '1,180p' frontend/src/api/http.jsRepository: ssafy-salman/salmanhae
Length of output: 2595
Keep the refresh call on the client timeout. axios.post bypasses the 10s timeout on http, so a stalled refresh can leave isRefreshing stuck and queued requests waiting. Use http.post or pass the same timeout here.
🤖 Prompt for 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.
In `@frontend/src/api/http.js` around lines 76 - 78, The refresh request in the
auth flow is bypassing the shared client timeout because it uses axios.post
directly. Update the refresh logic in http.js so the refresh call goes through
the existing http client or explicitly applies the same 10s timeout, keeping the
request bounded and preventing isRefreshing from getting stuck in the refresh
path.
| localStorage.removeItem(ACCESS_KEY) | ||
| localStorage.removeItem(REFRESH_KEY) | ||
| window.location.href = '/login' |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Remove the legacy access token on refresh failure.
Because Line 22 still reads accessToken as a fallback, removing only the new keys can leave stale credentials attached after redirecting to login.
Proposed fix
const ACCESS_KEY = 'salmanhae.accessToken'
const REFRESH_KEY = 'salmanhae.refreshToken'
+const LEGACY_ACCESS_KEY = 'accessToken'
@@
localStorage.removeItem(ACCESS_KEY)
localStorage.removeItem(REFRESH_KEY)
+ localStorage.removeItem(LEGACY_ACCESS_KEY)
window.location.href = '/login'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| localStorage.removeItem(ACCESS_KEY) | |
| localStorage.removeItem(REFRESH_KEY) | |
| window.location.href = '/login' | |
| const ACCESS_KEY = 'salmanhae.accessToken' | |
| const REFRESH_KEY = 'salmanhae.refreshToken' | |
| const LEGACY_ACCESS_KEY = 'accessToken' |
| localStorage.removeItem(ACCESS_KEY) | |
| localStorage.removeItem(REFRESH_KEY) | |
| window.location.href = '/login' | |
| localStorage.removeItem(ACCESS_KEY) | |
| localStorage.removeItem(REFRESH_KEY) | |
| localStorage.removeItem(LEGACY_ACCESS_KEY) | |
| window.location.href = '/login' |
🤖 Prompt for 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.
In `@frontend/src/api/http.js` around lines 88 - 90, The refresh-failure cleanup
in the HTTP API flow only removes the new storage keys, leaving the legacy
accessToken fallback behind. Update the error handling around the token refresh
path in http.js to also clear the old access token entry when redirecting to
/login, keeping the cleanup consistent with the accessToken lookup used earlier
in the module.
| _saveTokens(accessToken, refreshToken) { | ||
| this.accessToken = accessToken | ||
| this.refreshToken = refreshToken | ||
| localStorage.setItem(ACCESS_KEY, accessToken) | ||
| localStorage.setItem(REFRESH_KEY, refreshToken) | ||
| }, |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Avoid persisting refresh tokens in localStorage.
Line 28 stores the refresh token in JS-readable storage, so any XSS can steal a token that renews the session. Prefer an HttpOnly/SameSite cookie for refresh and keep only the short-lived access token in JS-managed state.
🤖 Prompt for 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.
In `@frontend/src/store/authStore.js` around lines 24 - 29, The _saveTokens method
in authStore is persisting the refreshToken to localStorage, which should be
removed. Keep only the accessToken in JS-managed state/storage and update the
refresh flow to use an HttpOnly/SameSite cookie instead of exposing the refresh
token in localStorage. Ensure any callers of _saveTokens and related auth token
handling still work with the refresh token no longer being stored client-side.
| _clearTokens() { | ||
| this.user = null | ||
| this.accessToken = '' | ||
| this.refreshToken = '' | ||
| localStorage.removeItem(ACCESS_KEY) | ||
| localStorage.removeItem(REFRESH_KEY) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Clear the legacy access-token key too.
http.js still falls back to localStorage.getItem('accessToken'), so logout can clear the new keys while leaving an old token available to the HTTP client.
Proposed fix
const ACCESS_KEY = 'salmanhae.accessToken'
const REFRESH_KEY = 'salmanhae.refreshToken'
+const LEGACY_ACCESS_KEY = 'accessToken'
@@
localStorage.removeItem(ACCESS_KEY)
localStorage.removeItem(REFRESH_KEY)
+ localStorage.removeItem(LEGACY_ACCESS_KEY)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| _clearTokens() { | |
| this.user = null | |
| this.accessToken = '' | |
| this.refreshToken = '' | |
| localStorage.removeItem(ACCESS_KEY) | |
| localStorage.removeItem(REFRESH_KEY) | |
| const LEGACY_ACCESS_KEY = 'accessToken' | |
| _clearTokens() { | |
| this.user = null | |
| this.accessToken = '' | |
| this.refreshToken = '' | |
| localStorage.removeItem(ACCESS_KEY) | |
| localStorage.removeItem(REFRESH_KEY) | |
| localStorage.removeItem(LEGACY_ACCESS_KEY) |
🤖 Prompt for 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.
In `@frontend/src/store/authStore.js` around lines 31 - 36, The logout cleanup in
_clearTokens only removes the new storage keys, but the HTTP client still reads
the legacy accessToken fallback. Update _clearTokens in authStore to also remove
the old access-token key used by http.js, alongside ACCESS_KEY and REFRESH_KEY,
so stale tokens cannot survive logout.
변경 내용
AuthView.vue로그인 폼 →authStore.login()연결sendVerificationEmail()호출verifyEmail()호출authStore.signup()호출INVALID_VERIFICATION_CODE,EMAIL_ALREADY_EXISTS,EMAIL_NOT_VERIFIED)연결 이슈
closes #84
테스트
Summary by CodeRabbit