diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js new file mode 100644 index 0000000..79805a2 --- /dev/null +++ b/frontend/src/api/auth.js @@ -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 }) diff --git a/frontend/src/api/http.js b/frontend/src/api/http.js index a856572..75758c0 100644 --- a/frontend/src/api/http.js +++ b/frontend/src/api/http.js @@ -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) + } 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 diff --git a/frontend/src/store/authStore.js b/frontend/src/store/authStore.js new file mode 100644 index 0000000..cd5316d --- /dev/null +++ b/frontend/src/store/authStore.js @@ -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) || '' + }, + + _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) + }, + }, +}) diff --git a/frontend/src/views/AuthView.vue b/frontend/src/views/AuthView.vue index 5aaa017..87abcae 100644 --- a/frontend/src/views/AuthView.vue +++ b/frontend/src/views/AuthView.vue @@ -64,29 +64,66 @@