From 32c37610f68e06bfc0d02f1b417d6d02f3d68975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=8F=D0=BA=D0=B8=D0=BD=20=D0=90=D0=B9?= =?UTF-8?q?=D1=81=D0=B5=D0=BD?= Date: Mon, 18 Nov 2024 21:31:35 +0300 Subject: [PATCH] Login JWT done --- frontend/package-lock.json | 100 +++++++++++++++++ frontend/package.json | 2 + frontend/src/App.vue | 26 ++++- frontend/src/assets/css/main.css | 6 +- frontend/src/assets/svg/dot.svg | 3 + frontend/src/assets/svg/sidebar/right.svg | 4 + frontend/src/assets/svg/spinner.svg | 44 ++++++++ frontend/src/components/AdminSideBar.vue | 79 ++++++++++++++ frontend/src/components/TheFooter.vue | 44 ++++++++ frontend/src/components/TheHeader.vue | 4 + frontend/src/components/TheNavbar.vue | 50 ++++++++- frontend/src/components/UploadProgress.vue | 101 ++++++++++++++++++ frontend/src/components/modals/LoginModal.vue | 54 ++++++++-- .../src/components/tables/OrdersTable.vue | 91 ++++++++++++++++ frontend/src/js/axiosConfig.js | 38 +++++++ frontend/src/js/router.js | 6 ++ frontend/src/js/store.js | 2 + frontend/src/main.js | 3 + frontend/src/services/authService.js | 76 +++++++++++++ frontend/src/views/AdminOrderView.vue | 16 +++ frontend/src/views/HomeView.vue | 3 +- 21 files changed, 736 insertions(+), 16 deletions(-) create mode 100644 frontend/src/assets/svg/dot.svg create mode 100644 frontend/src/assets/svg/sidebar/right.svg create mode 100644 frontend/src/assets/svg/spinner.svg create mode 100644 frontend/src/components/AdminSideBar.vue create mode 100644 frontend/src/components/TheFooter.vue create mode 100644 frontend/src/components/UploadProgress.vue create mode 100644 frontend/src/components/tables/OrdersTable.vue create mode 100644 frontend/src/js/axiosConfig.js create mode 100644 frontend/src/services/authService.js create mode 100644 frontend/src/views/AdminOrderView.vue diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9a0e786..fbf1bfb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,8 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "axios": "^1.7.7", + "jwt-decode": "^4.0.0", "vue": "^3.5.12", "vue-router": "^4.4.5", "vuex": "^4.1.0" @@ -1337,6 +1339,21 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/birpc": { "version": "0.2.19", "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", @@ -1413,6 +1430,17 @@ } ] }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1510,6 +1538,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.57", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.57.tgz", @@ -1611,6 +1647,38 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", @@ -1822,6 +1890,14 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/kolorist": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", @@ -1851,6 +1927,25 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -2032,6 +2127,11 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9f19332..3d52cfa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,8 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.7.7", + "jwt-decode": "^4.0.0", "vue": "^3.5.12", "vue-router": "^4.4.5", "vuex": "^4.1.0" diff --git a/frontend/src/App.vue b/frontend/src/App.vue index f78a892..6971cc5 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,9 +1,29 @@ + + diff --git a/frontend/src/assets/css/main.css b/frontend/src/assets/css/main.css index 8abeeaa..16b33c6 100644 --- a/frontend/src/assets/css/main.css +++ b/frontend/src/assets/css/main.css @@ -2,7 +2,6 @@ #app { max-width: 1280px; - min-height: 100vh; margin: 0 auto; font-weight: normal; background-color: #FFFFFF; @@ -13,4 +12,9 @@ body { background-color: #D9D9D9; } +html, body { + height: 100%; + margin: 0; +} + diff --git a/frontend/src/assets/svg/dot.svg b/frontend/src/assets/svg/dot.svg new file mode 100644 index 0000000..4a50103 --- /dev/null +++ b/frontend/src/assets/svg/dot.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/svg/sidebar/right.svg b/frontend/src/assets/svg/sidebar/right.svg new file mode 100644 index 0000000..6b90137 --- /dev/null +++ b/frontend/src/assets/svg/sidebar/right.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/src/assets/svg/spinner.svg b/frontend/src/assets/svg/spinner.svg new file mode 100644 index 0000000..3f02fa3 --- /dev/null +++ b/frontend/src/assets/svg/spinner.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/AdminSideBar.vue b/frontend/src/components/AdminSideBar.vue new file mode 100644 index 0000000..038ffbf --- /dev/null +++ b/frontend/src/components/AdminSideBar.vue @@ -0,0 +1,79 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/TheFooter.vue b/frontend/src/components/TheFooter.vue new file mode 100644 index 0000000..78e00fe --- /dev/null +++ b/frontend/src/components/TheFooter.vue @@ -0,0 +1,44 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/TheHeader.vue b/frontend/src/components/TheHeader.vue index 407590a..16247c2 100644 --- a/frontend/src/components/TheHeader.vue +++ b/frontend/src/components/TheHeader.vue @@ -43,6 +43,10 @@ export default { .header { width: 100%; border-bottom: 1px solid rgba(0, 0, 0, 0.2); + position: sticky; + top: 0; + background-color: #FFFFFF; + z-index: 1000; } .content { diff --git a/frontend/src/components/TheNavbar.vue b/frontend/src/components/TheNavbar.vue index 30dd193..3a2ff2c 100644 --- a/frontend/src/components/TheNavbar.vue +++ b/frontend/src/components/TheNavbar.vue @@ -4,10 +4,58 @@ \ No newline at end of file diff --git a/frontend/src/components/UploadProgress.vue b/frontend/src/components/UploadProgress.vue new file mode 100644 index 0000000..5e3d2a7 --- /dev/null +++ b/frontend/src/components/UploadProgress.vue @@ -0,0 +1,101 @@ + + + + diff --git a/frontend/src/components/modals/LoginModal.vue b/frontend/src/components/modals/LoginModal.vue index 80dd26e..c9c03b0 100644 --- a/frontend/src/components/modals/LoginModal.vue +++ b/frontend/src/components/modals/LoginModal.vue @@ -33,18 +33,18 @@ -
-
@@ -75,17 +75,19 @@
+ + + \ No newline at end of file diff --git a/frontend/src/js/axiosConfig.js b/frontend/src/js/axiosConfig.js new file mode 100644 index 0000000..b69bfd4 --- /dev/null +++ b/frontend/src/js/axiosConfig.js @@ -0,0 +1,38 @@ +import axios from 'axios'; +import {getAccessToken, logoutUser, refreshToken} from "@/services/authService.js"; + +// Устанавливаем интерцептор для автоматической установки заголовка +axios.interceptors.request.use( + async (config) => { + const token = getAccessToken(); + if (token) { + config.headers['Authorization'] = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Интерцептор ответа для обновления токена при 401 ошибке +axios.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + + if (error.response.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + try { + const newToken = await refreshToken(); + axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; + originalRequest.headers['Authorization'] = `Bearer ${newToken}`; + return axios(originalRequest); + } catch (error) { + logoutUser(); + window.location.href = '/'; + } + } + return Promise.reject(error); + } +); \ No newline at end of file diff --git a/frontend/src/js/router.js b/frontend/src/js/router.js index 29788a1..28f07fb 100644 --- a/frontend/src/js/router.js +++ b/frontend/src/js/router.js @@ -1,5 +1,6 @@ import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' +import AdminOrderView from "@/views/AdminOrderView.vue"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -9,6 +10,11 @@ const router = createRouter({ name: 'home', component: HomeView, }, + { + path: '/admin/orders', + name:'admin-orders', + component: AdminOrderView, + } ], }) diff --git a/frontend/src/js/store.js b/frontend/src/js/store.js index aab570e..ea3831c 100644 --- a/frontend/src/js/store.js +++ b/frontend/src/js/store.js @@ -3,6 +3,7 @@ import { createStore } from 'vuex'; const store = createStore({ state: { isAuthenticated: false, + isAdmin: false, }, mutations: { login(state) { @@ -14,6 +15,7 @@ const store = createStore({ }, getters: { isAuthenticated: (state) => state.isAuthenticated, + isAdmin: (state) => state.isAdmin, }, }); diff --git a/frontend/src/main.js b/frontend/src/main.js index 1401d3a..cf3d0a4 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,11 +1,14 @@ import './assets/css/main.css' +import './js/axiosConfig.js' import { createApp } from 'vue' import App from './App.vue' import router from './js/router.js' +import store from './js/store.js' const app = createApp(App) app.use(router) +app.use(store) app.mount('#app') diff --git a/frontend/src/services/authService.js b/frontend/src/services/authService.js new file mode 100644 index 0000000..a9a1712 --- /dev/null +++ b/frontend/src/services/authService.js @@ -0,0 +1,76 @@ +import axios from 'axios'; +import {jwtDecode} from "jwt-decode"; +import store from "@/js/store.js"; + +const API_URL = 'http://localhost:3000/api/auth'; + +export const register = async (userData) => { + try{ + const response = await axios.post(`${API_URL}/register/client`, userData); + return { data: response.data, error: null}; + } catch (error) { + const errorMessage = error.response + ? error.response.data // Если есть ответ от сервера + : error.message || 'Ошибка соединения'; + return { data: null, error: errorMessage} + } +}; + +export const login = async (userData) => { + try{ + const response = await axios.post(`${API_URL}/token`, userData); + return { data: response.data, error: null} + } catch (error) { + const errorMessage = error.response + ? error.response.data // Если есть ответ от сервера + : error.message || 'Ошибка соединения'; + return { data: null, error: errorMessage} + } +}; + +export const storeUserInfo = (token) => { + storeAccessToken(token); + const decodedToken = jwtDecode(token); + const isAdmin = (decodedToken.role === "admin") || false; + const isAuthenticated = true; + + localStorage.setItem('isAdmin', isAdmin.toString()); + localStorage.setItem('isAuthenticated', isAuthenticated.toString()); +} + +export const isUserAdmin = () => { + return localStorage.getItem('isAdmin') === 'true'; +}; + +export const isUserAuthenticated = () => { + return localStorage.getItem('isAuthenticated') === 'true'; +}; + +export const storeAccessToken = (token) => { + localStorage.setItem('access_token', token); +}; + +export const getAccessToken = () => { + return localStorage.getItem('access_token'); +}; + +export const logoutUser = () => { + localStorage.removeItem('access_token'); + localStorage.removeItem('isAdmin'); + localStorage.removeItem('isAuthenticated'); +}; + +export const refreshToken = async () => { + try { + const response = await axios.post('/api/auth/token/refresh', null, { + withCredentials: true // Указывает, что куки (refresh-токен) должны быть отправлены с запросом + }); + const { access_token } = response.data; + storeAccessToken(access_token); + storeUserInfo(access_token); + return access_token; + } catch (error) { + console.error('Ошибка обновления токена:', error); + throw error; + } +}; diff --git a/frontend/src/views/AdminOrderView.vue b/frontend/src/views/AdminOrderView.vue new file mode 100644 index 0000000..92fd7be --- /dev/null +++ b/frontend/src/views/AdminOrderView.vue @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 37f2386..de20965 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -1,8 +1,9 @@