From f66c014a36adb269d21849bb225761e0b715eb0e Mon Sep 17 00:00:00 2001 From: nquidox Date: Wed, 10 Sep 2025 23:29:27 +0300 Subject: [PATCH] added: login, logout and auto refresh token --- src/App.vue | 9 ++- src/main.js | 2 - src/router/index.js | 6 +- src/services/apiClient.js | 118 ++++++++++++++++++++++++++++-- src/services/setupInterceptors.js | 12 --- src/stores/authStore.js | 58 ++++++++++----- src/views/HomeView.vue | 7 -- src/views/LoginView.vue | 20 ++--- src/views/StartPageView.vue | 20 +++++ 9 files changed, 189 insertions(+), 63 deletions(-) delete mode 100644 src/services/setupInterceptors.js delete mode 100644 src/views/HomeView.vue create mode 100644 src/views/StartPageView.vue diff --git a/src/App.vue b/src/App.vue index da9d61a..51d8927 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,10 +2,15 @@ + diff --git a/src/main.js b/src/main.js index f71bc34..fda1e6e 100644 --- a/src/main.js +++ b/src/main.js @@ -3,12 +3,10 @@ import { createPinia } from 'pinia' import App from './App.vue' import router from './router' -import { setupAuthInterceptor } from '@/services/setupInterceptors.js' const app = createApp(App) app.use(createPinia()) app.use(router) -setupAuthInterceptor() app.mount('#app') diff --git a/src/router/index.js b/src/router/index.js index 3e2ff9d..b6eb2a6 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,5 +1,5 @@ import { createRouter, createWebHistory } from 'vue-router' -import HomeView from '../views/HomeView.vue' +import StartPageView from '../views/StartPageView.vue' import LoginView from '@/views/LoginView.vue' const router = createRouter({ @@ -7,8 +7,8 @@ const router = createRouter({ routes: [ { path: '/', - name: 'home', - component: HomeView, + name: 'startPage', + component: StartPageView, }, { path: '/login', diff --git a/src/services/apiClient.js b/src/services/apiClient.js index 7844a0b..cd53f97 100644 --- a/src/services/apiClient.js +++ b/src/services/apiClient.js @@ -1,11 +1,113 @@ -import axios from 'axios' +import { useAuthStore } from '@/stores/authStore.js'; -const apiClient = axios.create({ - baseURL: 'http://localhost:9000/api/v2', - timeout: 5000, - headers: { +const BASE_URL = 'http://localhost:9000/api/v2'; + +let isRefreshing = false; +let refreshPromise = null; + +function createConfig(options = {}) { + const authStore = useAuthStore(); + const headers = { 'Content-Type': 'application/json', - } -}) + ...options.headers, + }; -export default apiClient + if (authStore.accessToken) { + headers['Authorization'] = `Bearer ${authStore.accessToken}`; + } + + return { + headers, + credentials: 'include', + ...options, + }; +} + +async function refreshAccessToken() { + const authStore = useAuthStore(); + + if (isRefreshing) return refreshPromise; + + isRefreshing = true; + refreshPromise = fetch(`${BASE_URL}/user/auth/refresh`, { + method: 'POST', + credentials: 'include', + }) + .then(async (res) => { + if (!res.ok) throw new Error('Failed to refresh access token'); + return res.json(); + }) + .then((data) => { + authStore.setToken(data.accessToken); + return data; + }) + .catch((error) => { + throw error; + }) + .finally(() => { + isRefreshing = false; + refreshPromise = null; + }); + + return refreshPromise; +} + +async function request(url, options = {}, isRetry = false) { + const config = createConfig(options); + + const response = await fetch(`${BASE_URL}${url}`, config); + + if (response.status === 401 && !isRetry) { + try { + const data = await refreshAccessToken(); + + const newOptions = { + ...options, + headers: { + ...options.headers, + 'Authorization': `Bearer ${data.accessToken}`, + }, + }; + return await request(url, newOptions, true); + } catch (e) { + const authStore = useAuthStore(); + authStore.forceLogout(); + throw e; + } + } + + if (!response.ok) { + let errorData; + try { + errorData = await response.json(); + } catch { + errorData = {}; + } + throw new Error(errorData.message || `HTTP Error: ${response.status}`); + } + + const contentType = response.headers.get('content-type'); + if (contentType?.includes('application/json')) { + try { + return await response.json(); + } catch (e) { + console.warn('Failed to parse JSON response', e); + return null; + } + } + + return null; +} + +export const apiClient = { + get: (url) => request(url, { method: 'GET' }), + post: (url, data) => request(url, { + method: 'POST', + body: JSON.stringify(data), + }), + put: (url, data) => request(url, { + method: 'PUT', + body: JSON.stringify(data), + }), + delete: (url) => request(url, { method: 'DELETE' }), +}; diff --git a/src/services/setupInterceptors.js b/src/services/setupInterceptors.js deleted file mode 100644 index 48024e5..0000000 --- a/src/services/setupInterceptors.js +++ /dev/null @@ -1,12 +0,0 @@ -import apiClient from '@/services/apiClient' -import { useAuthStore } from '@/stores/authStore' - -export function setupAuthInterceptor() { - apiClient.interceptors.request.use((config) => { - const authStore = useAuthStore() - if (authStore.accessToken) { - config.headers.Authorization = `Bearer ${authStore.accessToken}` - } - return config - }) -} diff --git a/src/stores/authStore.js b/src/stores/authStore.js index 180cd10..2ce7d63 100644 --- a/src/stores/authStore.js +++ b/src/stores/authStore.js @@ -1,41 +1,59 @@ import { defineStore } from 'pinia'; import { computed, ref } from 'vue'; -import apiClient from '@/services/apiClient'; +import { apiClient } from '@/services/apiClient'; +import router from '@/router/index.js'; export const useAuthStore = defineStore('auth', () => { // state - const accessToken = ref(null) - const user = ref(null) + const accessToken = ref(null); + const user = ref(null); // getters - const isAuthenticated = computed(() => !!accessToken.value) + const isAuthenticated = computed(() => !!accessToken.value); // actions + const setToken = (token) => { + accessToken.value = token; + }; + const login = async (email, password) => { try { - const response = await apiClient.post( - "/user/login", - { email, password } - ) - const { access_token } = response.data - accessToken.value = access_token + const response = await apiClient.post('/user/auth/login', { email, password }); + const { access_token, user: userData } = response; - console.log('Email', email) - console.log('Password', password) + setToken(access_token); + user.value = userData || null; + + router.push('/'); } catch (error) { - console.log(error) + console.error('Login error:', error); } - } + }; - const logout = () => { - console.log('logout placeholder') - } + const logout = async () => { + accessToken.value = null; + user.value = null; + try { + await apiClient.post('/user/auth/logout'); + } catch (error) { + console.error('Logout error:', error); + } + router.push('/startPage'); + }; + + const forceLogout = () => { + accessToken.value = null; + user.value = null; + router.push('/startPage'); + }; return { accessToken, user, isAuthenticated, + setToken, login, - logout - } -}) + logout, + forceLogout, + }; +}); diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue deleted file mode 100644 index 3b786e1..0000000 --- a/src/views/HomeView.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index eb858ec..ecb8222 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -1,16 +1,16 @@ - diff --git a/src/views/StartPageView.vue b/src/views/StartPageView.vue new file mode 100644 index 0000000..161eb51 --- /dev/null +++ b/src/views/StartPageView.vue @@ -0,0 +1,20 @@ + + + + +