Skip to content

[Phase 3] feat(auth): 라우터 가드·헤더 로그인 상태 반영·세션 복원 구현 - #87

Merged
crolvlee merged 2 commits into
developfrom
phase/86-router-guard-header
Jun 25, 2026
Merged

[Phase 3] feat(auth): 라우터 가드·헤더 로그인 상태 반영·세션 복원 구현#87
crolvlee merged 2 commits into
developfrom
phase/86-router-guard-header

Conversation

@crolvlee

@crolvlee crolvlee commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

변경 내용

  • router/index.js: /chat 라우트에 meta: { requiresAuth: true } 추가, 비로그인 접근 시 /login 리다이렉트 가드
  • App.vue: 헤더에 로그인 상태 반영 — 비로그인 시 "로그인" 버튼, 로그인 시 "로그아웃" 버튼
  • main.js: 앱 초기화 시 authStore.restoreSession() 호출 (localStorage 토큰 복원)

연결 이슈

closes #86

테스트

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

Summary by CodeRabbit

  • New Features
    • Added login-aware header actions, showing “로그아웃” for signed-in users and “로그인” for signed-out users.
    • Protected the chat area so unauthenticated visitors are redirected to sign in, preserving the original destination.
  • Bug Fixes
    • Automatically restores the previous session on app startup.
    • Improved token refresh handling for multiple simultaneous requests, and ensures users are redirected to sign in if refresh fails.
    • Logout now reliably clears credentials and returns users to the home page.

@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:57am

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

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The frontend adds auth API helpers, session restoration, route protection, header logout controls, and a shared 401 refresh queue for access-token renewal.

Changes

Authentication bootstrap, routing, and tokens

Layer / File(s) Summary
Auth API and store state
frontend/src/api/auth.js, frontend/src/store/authStore.js
Adds auth endpoint wrappers and a Pinia auth store with token persistence, restore, login, logout, and signup actions.
Startup restore and route guard
frontend/src/main.js, frontend/src/router/index.js
Installs Pinia explicitly, restores session state before mount, marks /chat as protected, and redirects unauthenticated navigation to /login with the original destination.
Header auth CTA and logout
frontend/src/App.vue
Switches the header between login and logout controls based on auth state, triggers logout, and navigates back to /.
HTTP refresh queue and retry
frontend/src/api/http.js
Adds token key constants and a coordinated 401 refresh flow with queued retries, token persistence, and login redirect on refresh failure.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ssafy-salman/salmanhae#80: Shares the same auth-entry UI and route surface, including the header login area and /login-related navigation.
  • ssafy-salman/salmanhae#83: Covers the same auth API, store, and HTTP token-refresh machinery that this PR extends and wires into the app.

Poem

A bunny tuned the login gate with whiskers neat and bright,
Restored the tucked-away tokens before the day took flight.
If chat wants a guarded hop, the route will ask the way,
And lost refreshes hop back home before they fade away. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main auth routing, header, and session-restore changes.
Description check ✅ Passed The description matches the template well and includes change summary, linked issue, and test checklist.
Linked Issues check ✅ Passed The PR implements the Phase 3 router guard, login-state header, and session restore requested by issue #86.
Out of Scope Changes check ✅ Passed The added auth store, API, and HTTP refresh logic directly support the requested auth flow and appear 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/86-router-guard-header

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.

🧹 Nitpick comments (1)
frontend/src/App.vue (1)

39-42: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider guarding auth.logout() against rejection.

If auth.logout() rejects (e.g., a network error during server-side logout), router.push('/') is skipped and the user is left without feedback. A try/finally ensures the redirect still happens after local state is cleared.

Optional refactor
 async function handleLogout() {
-  await auth.logout()
-  router.push('/')
+  try {
+    await auth.logout()
+  } finally {
+    router.push('/')
+  }
 }
🤖 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/App.vue` around lines 39 - 42, The logout flow in handleLogout
currently lets a rejection from auth.logout() prevent the redirect, so update it
to ensure router.push('/') still runs even if the logout request fails. Use a
try/finally around auth.logout() in App.vue’s handleLogout function, keeping the
redirect in the finally block so the user is always sent back to the home route
after local cleanup.
🤖 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.

Nitpick comments:
In `@frontend/src/App.vue`:
- Around line 39-42: The logout flow in handleLogout currently lets a rejection
from auth.logout() prevent the redirect, so update it to ensure router.push('/')
still runs even if the logout request fails. Use a try/finally around
auth.logout() in App.vue’s handleLogout function, keeping the redirect in the
finally block so the user is always sent back to the home route after local
cleanup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 775114f7-d1a0-420c-8999-74ea0640e9a9

📥 Commits

Reviewing files that changed from the base of the PR and between 38199b5 and 5649ce7.

📒 Files selected for processing (3)
  • frontend/src/App.vue
  • frontend/src/main.js
  • frontend/src/router/index.js

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: 3

🧹 Nitpick comments (2)
frontend/src/store/authStore.js (1)

4-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Token storage keys duplicated across authStore.js and http.js.

ACCESS_KEY/REFRESH_KEY are redefined here with the same literal values as in frontend/src/api/http.js (lines 6-7). If one side changes a key string, the store and the HTTP refresh flow silently disagree on storage. Extract them into a shared module and import from both.

🤖 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 4 - 5, The token storage keys
are duplicated between authStore and the HTTP refresh flow, so both places can
drift if one literal changes. Move ACCESS_KEY and REFRESH_KEY into a shared
module and update authStore.js and http.js to import those shared constants
instead of redefining them locally, keeping the token names consistent across
the authStore and refresh logic.
frontend/src/api/http.js (1)

90-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hard redirect via window.location.href drops SPA state and the original destination.

A full page reload discards the in-memory app and doesn't preserve where the user was. Other navigation in this stack uses the router and passes query.redirect (see frontend/src/router/index.js). Prefer a router push (router.push({ path: '/login', query: { redirect: ... } })) for consistency, falling back to location.href only if the router isn't reachable 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` at line 90, The auth redirect in the HTTP handler
uses a hard reload via window.location.href, which drops SPA state and loses the
user’s original destination. Update the redirect logic in the HTTP client flow
to use the router-based navigation pattern used elsewhere, following the router
push approach in router/index.js and preserving the current destination in
query.redirect; only fall back to location.href if the router instance is
unavailable in this context.
🤖 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 63-69: Queued retries in the http interceptor are not being marked
as retried, so they can re-enter the 401 refresh flow and trigger another
refresh cycle. Update the pendingQueue resolution path in the http request
interceptor so the retried request from http(original) also sets original._retry
= true before reissuing it, matching the behavior used for the initial
refresh-triggering request and preventing repeat refresh attempts.
- Around line 76-85: The token refresh flow in http’s axios interceptor updates
localStorage and axios defaults but leaves the auth store stale, so useAuthStore
state and downstream auth checks stay out of sync. Update the refresh handling
in the response interceptor to also invoke the auth store’s token persistence
action (for example the store method that saves both accessToken and
refreshToken) right after parsing res.data.data, alongside the existing
localStorage and http.defaults updates. Keep the existing flushQueue and
original request retry logic in place, but make sure the store update happens in
the same refresh path so isLoggedIn, the router guard, and the header all read
the same tokens.

In `@frontend/src/store/authStore.js`:
- Around line 19-22: restoreSession currently rehydrates only accessToken and
refreshToken, leaving user null so authenticated UI can render without profile
data. Update authStore’s restoreSession to also restore the user state when
tokens are present, or trigger the existing profile fetch flow from the auth
store after restoring a valid token. Keep the change centered around
restoreSession and any related user-loading action in authStore.

---

Nitpick comments:
In `@frontend/src/api/http.js`:
- Line 90: The auth redirect in the HTTP handler uses a hard reload via
window.location.href, which drops SPA state and loses the user’s original
destination. Update the redirect logic in the HTTP client flow to use the
router-based navigation pattern used elsewhere, following the router push
approach in router/index.js and preserving the current destination in
query.redirect; only fall back to location.href if the router instance is
unavailable in this context.

In `@frontend/src/store/authStore.js`:
- Around line 4-5: The token storage keys are duplicated between authStore and
the HTTP refresh flow, so both places can drift if one literal changes. Move
ACCESS_KEY and REFRESH_KEY into a shared module and update authStore.js and
http.js to import those shared constants instead of redefining them locally,
keeping the token names consistent across the authStore and refresh logic.
🪄 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: 76edc980-8daf-46c8-b696-e3fd8404c740

📥 Commits

Reviewing files that changed from the base of the PR and between 5649ce7 and b4b4be9.

📒 Files selected for processing (3)
  • frontend/src/api/auth.js
  • frontend/src/api/http.js
  • frontend/src/store/authStore.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 | 🟡 Minor | ⚡ Quick win

Queued retries are not marked _retry, allowing repeat refresh attempts.

The request that triggers the refresh sets original._retry = true (line 72), but requests resolved from pendingQueue retry via http(original) without ever setting _retry. If the freshly issued token is still rejected with 401, those retried requests re-enter the interceptor, pass the !original._retry gate, and kick off another refresh cycle. Mark queued retries as retried as well.

Proposed guard
       }).then((token) => {
+        original._retry = true
         original.headers.Authorization = `Bearer ${token}`
         return http(original)
       })
📝 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) {
return new Promise((resolve, reject) => {
pendingQueue.push({ resolve, reject })
}).then((token) => {
original._retry = true
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 retries in the http
interceptor are not being marked as retried, so they can re-enter the 401
refresh flow and trigger another refresh cycle. Update the pendingQueue
resolution path in the http request interceptor so the retried request from
http(original) also sets original._retry = true before reissuing it, matching
the behavior used for the initial refresh-triggering request and preventing
repeat refresh attempts.

Comment thread frontend/src/api/http.js
Comment on lines +76 to +85
const res = await axios.post(`${baseURL}/api/v1/auth/refresh`, {
refreshToken: storedRefresh,
})
const { accessToken, refreshToken } = res.data.data
localStorage.setItem(ACCESS_KEY, accessToken)
localStorage.setItem(REFRESH_KEY, refreshToken)
http.defaults.headers.common.Authorization = `Bearer ${accessToken}`
flushQueue(null, accessToken)
original.headers.Authorization = `Bearer ${accessToken}`
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.

🗄️ Data Integrity & Integration | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find direct consumers of store token state that could observe a stale value
rg -nP '\.(accessToken|refreshToken)\b' frontend/src -g '!**/http.js' -C2
rg -nP '\bisLoggedIn\b' frontend/src -C2

Repository: ssafy-salman/salmanhae

Length of output: 2939


🏁 Script executed:

#!/bin/bash
set -e
# Inspect the refresh flow and auth store to see whether tokens are synchronized.
sed -n '1,220p' frontend/src/api/http.js
printf '\n--- authStore ---\n'
sed -n '1,220p' frontend/src/store/authStore.js
printf '\n--- router ---\n'
sed -n '1,120p' frontend/src/router/index.js
printf '\n--- App.vue ---\n'
sed -n '1,120p' frontend/src/App.vue

Repository: ssafy-salman/salmanhae

Length of output: 8093


🏁 Script executed:

#!/bin/bash
set -e
# Look for any refresh-specific store updates elsewhere.
rg -n "refreshToken|_saveTokens|restoreSession|flushQueue|window.location.href|location.href" frontend/src -C3

Repository: ssafy-salman/salmanhae

Length of output: 5465


🏁 Script executed:

#!/bin/bash
set -e
# Check whether anything outside the store reads accessToken/refreshToken directly.
rg -n "useAuthStore\(\)|auth\.(accessToken|refreshToken)|store\.accessToken|store\.refreshToken" frontend/src -C2

Repository: ssafy-salman/salmanhae

Length of output: 1532


🏁 Script executed:

#!/bin/bash
set -e
rg -n "useAuthStore\(\)|auth\.(accessToken|refreshToken)|store\.accessToken|store\.refreshToken" frontend/src -C2

Repository: ssafy-salman/salmanhae

Length of output: 1532


🏁 Script executed:

#!/bin/bash
set -e
sed -n '1,160p' frontend/src/api/auth.js
printf '\n--- main.js ---\n'
sed -n '1,80p' frontend/src/main.js

Repository: ssafy-salman/salmanhae

Length of output: 1130


Sync the auth store during token refresh
frontend/src/api/http.js:76-85 updates localStorage and http.defaults, but useAuthStore().accessToken / refreshToken stay stale for the rest of the session. Call the store’s token save action here (for example _saveTokens(accessToken, refreshToken)) so isLoggedIn, the router guard, and the header stay in sync.

🤖 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 - 85, The token refresh flow in
http’s axios interceptor updates localStorage and axios defaults but leaves the
auth store stale, so useAuthStore state and downstream auth checks stay out of
sync. Update the refresh handling in the response interceptor to also invoke the
auth store’s token persistence action (for example the store method that saves
both accessToken and refreshToken) right after parsing res.data.data, alongside
the existing localStorage and http.defaults updates. Keep the existing
flushQueue and original request retry logic in place, but make sure the store
update happens in the same refresh path so isLoggedIn, the router guard, and the
header all read the same tokens.

Comment on lines +19 to +22
restoreSession() {
this.accessToken = localStorage.getItem(ACCESS_KEY) || ''
this.refreshToken = localStorage.getItem(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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

restoreSession rehydrates tokens but never restores user.

After a reload, accessToken is restored (so isLoggedIn is true) yet user stays null. Any header/profile UI that reads user will render an authenticated-but-empty state until a fresh fetch. Consider persisting/rehydrating user too, or fetching the profile when a token is restored.

🤖 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 19 - 22, restoreSession
currently rehydrates only accessToken and refreshToken, leaving user null so
authenticated UI can render without profile data. Update authStore’s
restoreSession to also restore the user state when tokens are present, or
trigger the existing profile fetch flow from the auth store after restoring a
valid token. Keep the change centered around restoreSession and any related
user-loading action in authStore.

@crolvlee
crolvlee merged commit f21fb87 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 3] router guard header

1 participant