-
Notifications
You must be signed in to change notification settings - Fork 0
[Phase 3] feat(auth): 라우터 가드·헤더 로그인 상태 반영·세션 복원 구현 #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import http from './http.js' | ||
|
|
||
| export const sendVerificationEmail = (email) => | ||
| http.post('/api/v1/auth/email/send', { email }) | ||
|
|
||
| export const verifyEmail = (email, code) => | ||
| http.post('/api/v1/auth/email/verify', { email, code }) | ||
|
|
||
| export const signup = (email, password, nickname) => | ||
| http.post('/api/v1/auth/signup', { email, password, nickname }) | ||
|
|
||
| export const login = (email, password) => | ||
| http.post('/api/v1/auth/login', { email, password }) | ||
|
|
||
| export const logout = () => | ||
| http.post('/api/v1/auth/logout') | ||
|
|
||
| export const refresh = (refreshToken) => | ||
| http.post('/api/v1/auth/refresh', { refreshToken }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,9 @@ import axios from 'axios' | |
| const viteEnv = import.meta.env || {} | ||
| const baseURL = viteEnv.VITE_API_BASE_URL || 'http://localhost:8080' | ||
|
|
||
| const ACCESS_KEY = 'salmanhae.accessToken' | ||
| const REFRESH_KEY = 'salmanhae.refreshToken' | ||
|
|
||
| const http = axios.create({ | ||
| baseURL, | ||
| timeout: 10000, | ||
|
|
@@ -15,8 +18,8 @@ const readAccessToken = () => { | |
| try { | ||
| if (typeof window === 'undefined' || !window.localStorage) return '' | ||
| return ( | ||
| window.localStorage.getItem(ACCESS_KEY) || | ||
| window.localStorage.getItem('accessToken') || | ||
| window.localStorage.getItem('salmanhae.accessToken') || | ||
| '' | ||
| ) | ||
| } catch { | ||
|
|
@@ -32,4 +35,64 @@ http.interceptors.request.use((config) => { | |
| return config | ||
| }) | ||
|
|
||
| let isRefreshing = false | ||
| let pendingQueue = [] | ||
|
|
||
| const flushQueue = (error, token = null) => { | ||
| pendingQueue.forEach(({ resolve, reject }) => { | ||
| if (error) reject(error) | ||
| else resolve(token) | ||
| }) | ||
| pendingQueue = [] | ||
| } | ||
|
|
||
| http.interceptors.response.use( | ||
| (res) => res, | ||
| async (error) => { | ||
| const original = error.config | ||
|
|
||
| if (error.response?.status !== 401 || original._retry) { | ||
| return Promise.reject(error) | ||
| } | ||
|
|
||
| const storedRefresh = localStorage.getItem(REFRESH_KEY) | ||
| if (!storedRefresh) { | ||
| return Promise.reject(error) | ||
| } | ||
|
|
||
| if (isRefreshing) { | ||
| return new Promise((resolve, reject) => { | ||
| pendingQueue.push({ resolve, reject }) | ||
| }).then((token) => { | ||
| original.headers.Authorization = `Bearer ${token}` | ||
| return http(original) | ||
| }) | ||
| } | ||
|
|
||
| original._retry = true | ||
| isRefreshing = true | ||
|
|
||
| try { | ||
| 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) | ||
|
Comment on lines
+76
to
+85
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 -C2Repository: 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.vueRepository: 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 -C3Repository: 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 -C2Repository: 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 -C2Repository: 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.jsRepository: ssafy-salman/salmanhae Length of output: 1130 Sync the auth store during token refresh 🤖 Prompt for AI Agents |
||
| } catch (refreshError) { | ||
| flushQueue(refreshError) | ||
| localStorage.removeItem(ACCESS_KEY) | ||
| localStorage.removeItem(REFRESH_KEY) | ||
| window.location.href = '/login' | ||
| return Promise.reject(refreshError) | ||
| } finally { | ||
| isRefreshing = false | ||
| } | ||
| } | ||
| ) | ||
|
|
||
| export default http | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { defineStore } from 'pinia' | ||
| import { login as apiLogin, logout as apiLogout, signup as apiSignup } from '../api/auth.js' | ||
|
|
||
| const ACCESS_KEY = 'salmanhae.accessToken' | ||
| const REFRESH_KEY = 'salmanhae.refreshToken' | ||
|
|
||
| export const useAuthStore = defineStore('auth', { | ||
| state: () => ({ | ||
| user: null, | ||
| accessToken: '', | ||
| refreshToken: '', | ||
| }), | ||
|
|
||
| getters: { | ||
| isLoggedIn: (state) => !!state.accessToken, | ||
| }, | ||
|
|
||
| actions: { | ||
| restoreSession() { | ||
| this.accessToken = localStorage.getItem(ACCESS_KEY) || '' | ||
| this.refreshToken = localStorage.getItem(REFRESH_KEY) || '' | ||
| }, | ||
|
Comment on lines
+19
to
+22
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
After a reload, 🤖 Prompt for AI Agents |
||
|
|
||
| _saveTokens(accessToken, refreshToken) { | ||
| this.accessToken = accessToken | ||
| this.refreshToken = refreshToken | ||
| localStorage.setItem(ACCESS_KEY, accessToken) | ||
| localStorage.setItem(REFRESH_KEY, refreshToken) | ||
| }, | ||
|
|
||
| _clearTokens() { | ||
| this.user = null | ||
| this.accessToken = '' | ||
| this.refreshToken = '' | ||
| localStorage.removeItem(ACCESS_KEY) | ||
| localStorage.removeItem(REFRESH_KEY) | ||
| }, | ||
|
|
||
| async login(email, password) { | ||
| const res = await apiLogin(email, password) | ||
| const { accessToken, refreshToken } = res.data.data | ||
| this._saveTokens(accessToken, refreshToken) | ||
| }, | ||
|
|
||
| async logout() { | ||
| try { | ||
| await apiLogout() | ||
| } finally { | ||
| this._clearTokens() | ||
| } | ||
| }, | ||
|
|
||
| async signup(email, password, nickname) { | ||
| await apiSignup(email, password, nickname) | ||
| }, | ||
| }, | ||
| }) | ||
There was a problem hiding this comment.
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 frompendingQueueretry viahttp(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._retrygate, 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
🤖 Prompt for AI Agents