Skip to content

[Phase 2] feat(auth): AuthView 로그인·회원가입 이메일 인증 3단계 API 연결 - #85

Merged
crolvlee merged 2 commits into
developfrom
phase/84-authview-connect
Jun 25, 2026
Merged

[Phase 2] feat(auth): AuthView 로그인·회원가입 이메일 인증 3단계 API 연결#85
crolvlee merged 2 commits into
developfrom
phase/84-authview-connect

Conversation

@crolvlee

@crolvlee crolvlee commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

변경 내용

  • AuthView.vue 로그인 폼 → authStore.login() 연결
  • 회원가입 이메일 인증 3단계 추가:
    • Step 1: 이메일 입력 → sendVerificationEmail() 호출
    • Step 2: 6자리 인증코드 입력 → verifyEmail() 호출
    • Step 3: 닉네임·비밀번호 입력 → authStore.signup() 호출
  • 에러코드별 한국어 메시지 처리 (INVALID_VERIFICATION_CODE, EMAIL_ALREADY_EXISTS, EMAIL_NOT_VERIFIED)

연결 이슈

closes #84

테스트

  • 단위 테스트 통과
  • 로컬 동작 확인
  • API 응답 형식 확인 (docs/08_API_SPEC.md)

Summary by CodeRabbit

  • New Features
    • Added a multi-step sign-up flow with email verification (email entry → code verification → nickname and registration).
  • Bug Fixes
    • Improved consistency of sign-up/login/verification error messages.
    • Switching between login and sign-up now resets the signup flow state.
  • Security & Session Improvements
    • Enhanced session handling with access/refresh token support and automatic token refresh.
    • On refresh failure, the app clears tokens and returns the user to the login screen.

@crolvlee crolvlee added the ai-generated Created by AI harness automation label Jun 25, 2026
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
salmanhae Ready Ready Preview, Comment Jun 25, 2026 2:54am

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

AuthView.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.

Changes

Auth signup and token flow

Layer / File(s) Summary
Auth API helpers
frontend/src/api/auth.js
api/auth.js exports request helpers for email verification, signup, login, logout, and refresh endpoints.
Auth store session handling
frontend/src/store/authStore.js
authStore.js defines session state, token persistence helpers, login and logout actions, and a signup action that calls the auth API.
Step-based signup UI and handlers
frontend/src/views/AuthView.vue
AuthView.vue replaces the single signup form with step-based email verification, adds shared auth error parsing, resets signup state on mode switches, and submits the final registration through the auth store.
Token refresh interceptor
frontend/src/api/http.js
http.js adds dedicated token keys, reads the new access-token storage first, and handles concurrent 401 responses with queued refresh and retry logic.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ssafy-salman/salmanhae#80: Adds the earlier AuthView login/register screen that this PR extends with multi-step email verification and final signup wiring.
  • ssafy-salman/salmanhae#83: Introduces the auth infrastructure this PR builds on, including the auth store and HTTP token handling paths.
  • ssafy-salman/salmanhae#84: Directly related phase task for the AuthView connection work implemented here.

Poem

🐰 I hopped through email, code, and name,
Then tucked new tokens in the frame.
A refresh whisked the 401 away,
And login waited for another day.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the AuthView login and 3-step email-verification signup work.
Description check ✅ Passed The description follows the template and includes the main changes, linked issue, and testing checklist.
Linked Issues check ✅ Passed The PR matches the directly linked Phase 2 authview-connect task and implements the described AuthView integration work.
Out of Scope Changes check ✅ Passed The auth API, store, and interceptor changes support the AuthView flow and appear to be in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase/84-authview-connect

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 38199b5 and 070ab54.

📒 Files selected for processing (1)
  • frontend/src/views/AuthView.vue

Comment on lines +100 to 107
<input
id="reg-code"
v-model="verifyCode"
type="text"
placeholder="홍길동"
autocomplete="name"
placeholder="6자리 코드 입력"
maxlength="6"
required
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
<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.

Comment on lines +247 to +250
await auth.signup(email.value, password.value, nickname.value)
switchMode('login')
} catch (e) {
errorMsg.value = e?.response?.data?.message ?? '회원가입에 실패했습니다.'
errorMsg.value = parseError(e, '회원가입에 실패했습니다.')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 070ab54 and 7432d68.

📒 Files selected for processing (3)
  • frontend/src/api/auth.js
  • frontend/src/api/http.js
  • frontend/src/store/authStore.js
✅ Files skipped from review due to trivial changes (1)
  • frontend/src/api/auth.js

Comment thread frontend/src/api/http.js
Comment on lines +63 to +69
if (isRefreshing) {
return new Promise((resolve, reject) => {
pendingQueue.push({ resolve, reject })
}).then((token) => {
original.headers.Authorization = `Bearer ${token}`
return http(original)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 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.

Suggested change
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.

Comment thread frontend/src/api/http.js
Comment on lines +76 to +78
const res = await axios.post(`${baseURL}/api/v1/auth/refresh`, {
refreshToken: storedRefresh,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 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' frontend

Repository: ssafy-salman/salmanhae

Length of output: 500


🏁 Script executed:

sed -n '1,180p' frontend/src/api/http.js

Repository: ssafy-salman/salmanhae

Length of output: 2595


🏁 Script executed:

sed -n '1,180p' frontend/src/api/http.js

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

Comment thread frontend/src/api/http.js
Comment on lines +88 to +90
localStorage.removeItem(ACCESS_KEY)
localStorage.removeItem(REFRESH_KEY)
window.location.href = '/login'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 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.

Suggested change
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'
Suggested change
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.

Comment on lines +24 to +29
_saveTokens(accessToken, refreshToken) {
this.accessToken = accessToken
this.refreshToken = refreshToken
localStorage.setItem(ACCESS_KEY, accessToken)
localStorage.setItem(REFRESH_KEY, refreshToken)
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 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.

Comment on lines +31 to +36
_clearTokens() {
this.user = null
this.accessToken = ''
this.refreshToken = ''
localStorage.removeItem(ACCESS_KEY)
localStorage.removeItem(REFRESH_KEY)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 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.

Suggested change
_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.

@crolvlee
crolvlee merged commit dd1db82 into develop Jun 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-generated Created by AI harness automation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase 2] authview connect

1 participant