@cryptotaxi247 / CoPilot / commits / ed375e63

updated refresh token flow

Davide Di Modica committed Oct 12, 2023 at 18:29 UTC ed375e63720f51180687378806235adb821e34a4
6 files changed +162 -70
package-lock.json
+17
@@ -8,6 +8,7 @@
8 "name": "copilot",
9 "version": "1.0.0",
10 "dependencies": {
11 + "@ajoelp/json-to-formdata": "^1.5.0",
12 "@animxyz/core": "^0.6.6",
13 "@animxyz/vue3": "^0.6.7",
14 "@fawmi/vue-google-maps": "^0.9.79",
@@ -148,6 +149,14 @@
149 "node": ">=0.10.0"
150 }
151 },
152 + "node_modules/@ajoelp/json-to-formdata": {
153 + "version": "1.5.0",
154 + "resolved": "https://registry.npmjs.org/@ajoelp/json-to-formdata/-/json-to-formdata-1.5.0.tgz",
155 + "integrity": "sha512-nrlfeTSL0X0dtx5r2KpzPiqLSIQquiiJjUKsQAKzWaCmO2QoYZCyb5ENZwF3YoffKronOCJr25mxaD8JRJmK8w==",
156 + "dependencies": {
157 + "lodash": "4.17.21"
158 + }
159 + },
160 "node_modules/@alloc/quick-lru": {
161 "version": "5.2.0",
162 "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -16603,6 +16612,14 @@
16612 "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
16613 "dev": true
16614 },
16615 + "@ajoelp/json-to-formdata": {
16616 + "version": "1.5.0",
16617 + "resolved": "https://registry.npmjs.org/@ajoelp/json-to-formdata/-/json-to-formdata-1.5.0.tgz",
16618 + "integrity": "sha512-nrlfeTSL0X0dtx5r2KpzPiqLSIQquiiJjUKsQAKzWaCmO2QoYZCyb5ENZwF3YoffKronOCJr25mxaD8JRJmK8w==",
16619 + "requires": {
16620 + "lodash": "4.17.21"
16621 + }
16622 + },
16623 "@alloc/quick-lru": {
16624 "version": "5.2.0",
16625 "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
package.json
+3 -2
@@ -9,7 +9,7 @@
9 }
10 },
11 "scripts": {
12 - "dev": "vite",
12 + "dev": "vite --host 127.0.0.1",
13 "dev:debug": "DEBUG=vite:* vite",
14 "build": "run-p type-check build-only",
15 "build-only": "vite build",
@@ -28,6 +28,7 @@
28 "open:redoc": "open http://127.0.0.1:5000/redoc"
29 },
30 "dependencies": {
31 + "@ajoelp/json-to-formdata": "^1.5.0",
32 "@animxyz/core": "^0.6.6",
33 "@animxyz/vue3": "^0.6.7",
34 "@fawmi/vue-google-maps": "^0.9.79",
@@ -158,4 +159,4 @@
159 "engines": {
160 "node": ">=16.0.0 <20.5.0"
161 }
161 -}
162 +}
\ No newline at end of file
src/App.vue
+9 -12
@@ -18,26 +18,19 @@
18 </template>
19
20 <script lang="ts" setup>
21 +import { computed, onBeforeMount, ref, type Component } from "vue"
22 import { useMainStore } from "@/stores/main"
23 +import { useThemeStore } from "@/stores/theme"
24 import VerticalNav from "@/layouts/VerticalNav"
25 import HorizontalNav from "@/layouts/HorizontalNav"
26 import Blank from "@/layouts/Blank"
27 import Provider from "@/layouts/common/Provider.vue"
28 import SplashScreen from "@/layouts/common/SplashScreen.vue"
29 import LayoutSettings from "@/components/LayoutSettings"
28 -import { useThemeStore } from "@/stores/theme"
30 import { Layout, RouterTransition, type ThemeName } from "@/types/theme.d"
30 -import { computed, onBeforeMount, ref } from "vue"
31 -import type { Component } from "vue"
32 -import { useRouter } from "vue-router"
33 -import { authCheck } from "@/middleware/auth.global"
34 -
31 +import { type RouteLocationNormalized, useRouter, useRoute } from "vue-router"
32 import "@/assets/scss/index.scss"
33
37 -defineOptions({
38 - name: "App"
39 -})
40 -
34 const router = useRouter()
35 const loading = ref(true)
36
@@ -54,17 +47,21 @@ const layoutComponent = computed<Component>(() => layoutComponents[forceLayout.v
47 const routerTransition = computed<RouterTransition>(() => useThemeStore().routerTransition)
48 const themeName = computed<ThemeName>(() => useThemeStore().themeName)
49
57 -router.beforeEach(route => {
50 +function checkForcedLayout(route: RouteLocationNormalized) {
51 if (route.meta?.forceLayout) {
52 forceLayout.value = route.meta.forceLayout
53 } else {
54 forceLayout.value = null
55 }
56 +}
57
64 - return authCheck(route)
58 +router.beforeEach(route => {
59 + checkForcedLayout(route)
60 })
61
62 onBeforeMount(() => {
63 + checkForcedLayout(useRoute())
64 +
65 setTimeout(() => {
66 loading.value = false
67 }, 500)
src/api/httpClient.ts
+42 -19
@@ -4,45 +4,68 @@ import Api from "@/api"
4 import axios from "axios"
5 const BASE_URL = import.meta.env.VITE_API_URL
6
7 -const store = useAuthStore()
8 -
7 const HttpClient = axios.create({
8 baseURL: BASE_URL
9 })
10
13 -HttpClient.interceptors.request.use(config => {
14 - if (!config.headers) config.headers = {}
15 - config.headers.token = store.user.token
11 +let __TOKEN_REFRESHING = false
12 +let __TOKEN_ATTEMPTS: number[] = []
13 +const TOKEN_MAX_ATTEMPTS = 3 // TODO: ?? replace with debounce time
14 +
15 +HttpClient.interceptors.request.use(
16 + config => {
17 + const store = useAuthStore()
18 +
19 + if (!config.headers) config.headers = {}
20 + config.headers.Authorization = `Bearer ${store.userToken}`
21
17 - if (isJwtExpiring(store.user.token, 60 * 60)) {
18 - Api.auth.refresh().then(res => {
19 - if (res.data.success && res.data.token) {
20 - store.setToken(res.data.token)
22 + if (isJwtExpiring(store.userToken, 60 * 60) && !__TOKEN_REFRESHING) {
23 + __TOKEN_REFRESHING = true
24 + __TOKEN_ATTEMPTS.push(new Date().getTime())
25 +
26 + if (__TOKEN_ATTEMPTS.length >= TOKEN_MAX_ATTEMPTS) {
27 + window.location.href = "/logout"
28 }
22 - })
23 - }
29
25 - return config
26 -})
30 + Api.auth.refresh().then(res => {
31 + if (res.data.access_token) {
32 + store.setToken(res.data.access_token)
33
28 -// TODO: to complete
29 -HttpClient.interceptors.response.use(
30 - response => {
31 - //if (response.config?.data?._retry) response.config.data._retry = false
34 + __TOKEN_REFRESHING = false
35 + __TOKEN_ATTEMPTS = []
36 + }
37 + })
38
33 - return response
39 + console.log("is expired")
40 + }
41 +
42 + console.log(__TOKEN_ATTEMPTS, __TOKEN_REFRESHING)
43 + return config
44 },
45 + error => Promise.reject(error)
46 +)
47 +
48 +// TODO: to complete
49 +HttpClient.interceptors.response.use(
50 + response => response,
51 error => {
52 + /*
53 if (error.response) {
54 if (error.response.status === 401 && !error.config.data?._retry) {
55 if (!error.config.data) error.config.data = {}
39 - //error.config.data._retry = true
56 + error.config.data._retry = true
57
58 if (window.location.pathname.indexOf("login") === -1) {
59 window.location.href = "/logout"
60 }
61 }
62 }
63 + */
64 + if (error.response && error.response.status === 401) {
65 + if (window.location.pathname.indexOf("login") === -1) {
66 + window.location.href = "/logout"
67 + }
68 + }
69
70 return Promise.reject(error)
71 }
src/utils/auth.ts
+56 -2
@@ -1,3 +1,7 @@
1 +import { useAuthStore } from "@/stores/auth"
2 +import { type RouteMetaAuth, UserRole } from "@/types/auth.d"
3 +import { type RouteLocationNormalized } from "vue-router"
4 +import _castArray from "lodash/castArray"
5 import * as jose from "jose"
6
7 /**
@@ -12,14 +16,64 @@ export function isJwtExpiring(token: string, threshold: number): boolean {
16
17 const { exp } = jose.decodeJwt(token)
18 const now = new Date().getTime() / 1000
15 - const gap = now + threshold
19 + const delta = (exp || 0) - threshold
20 +
21 + /*
22 + console.log("exp", new Date((exp || 0) * 1000))
23 + console.log("now", new Date())
24 + console.log("del", new Date(delta * 1000))
25 + */
26
27 if (!exp) {
28 return true
29 }
30
21 - return exp > now && exp < gap
31 + return now > delta && now < exp
32 } catch (err) {
33 return false
34 }
35 }
36 +
37 +export function authCheck(route: RouteLocationNormalized) {
38 + const meta: RouteMetaAuth = route.meta
39 + const { checkAuth, authRedirect, auth, roles } = meta
40 +
41 + if (route?.redirectedFrom?.name === "logout") {
42 + useAuthStore().setLogout()
43 + }
44 +
45 + if (auth === true) {
46 + if (!useAuthStore().isLogged) {
47 + window.location.href = "/login" + window.location.search
48 + return false
49 + }
50 +
51 + if (roles && !useAuthStore().isRoleGranted(roles)) {
52 + window.location.href = "/login" + window.location.search
53 + return false
54 + }
55 + }
56 +
57 + if (checkAuth === true) {
58 + if (useAuthStore().isLogged) {
59 + if (roles) {
60 + if (useAuthStore().isRoleGranted(roles)) {
61 + return authRedirect || "/"
62 + } else {
63 + return route.path
64 + }
65 + }
66 + return authRedirect || "/"
67 + }
68 + }
69 +}
70 +
71 +export function scopeToRole(scope: string | string[]): UserRole {
72 + const scopes: string[] = _castArray(scope)
73 + if (!scopes.length) return UserRole.Unknown
74 +
75 + if (scopes[0].toLowerCase() === "admin") return UserRole.Admin
76 + if (scopes[0].toLowerCase() === "analyst") return UserRole.Analyst
77 +
78 + return UserRole.Unknown
79 +}
unplugin.components.d.ts
+35 -35
@@ -5,39 +5,39 @@
5 // Read more: https://github.com/vuejs/core/pull/3399
6 export {}
7
8 -declare module "vue" {
9 - export interface GlobalComponents {
10 - CardActions: typeof import("./src/components/cards/CardActions.vue")["default"]
11 - CardBasic1: typeof import("./src/components/cards/basic/CardBasic1.vue")["default"]
12 - CardBasic2: typeof import("./src/components/cards/basic/CardBasic2.vue")["default"]
13 - CardBasic3: typeof import("./src/components/cards/basic/CardBasic3.vue")["default"]
14 - CardBasic4: typeof import("./src/components/cards/basic/CardBasic4.vue")["default"]
15 - CardBasic5: typeof import("./src/components/cards/basic/CardBasic5.vue")["default"]
16 - CardBasic6: typeof import("./src/components/cards/basic/CardBasic6.vue")["default"]
17 - CardCodeExample: typeof import("./src/components/cards/CardCodeExample.vue")["default"]
18 - CardCombo1: typeof import("./src/components/cards/combo/CardCombo1.vue")["default"]
19 - CardCombo2: typeof import("./src/components/cards/combo/CardCombo2.vue")["default"]
20 - CardCombo3: typeof import("./src/components/cards/combo/CardCombo3.vue")["default"]
21 - CardCombo4: typeof import("./src/components/cards/combo/CardCombo4.vue")["default"]
22 - CardCombo5: typeof import("./src/components/cards/combo/CardCombo5.vue")["default"]
23 - CardCombo6: typeof import("./src/components/cards/combo/CardCombo6.vue")["default"]
24 - CardCombo7: typeof import("./src/components/cards/combo/CardCombo7.vue")["default"]
25 - CardCombo8: typeof import("./src/components/cards/combo/CardCombo8.vue")["default"]
26 - CardComboIcon: typeof import("./src/components/cards/combo/CardComboIcon.vue")["default"]
27 - CardEcommerce1: typeof import("./src/components/cards/ecommerce/CardEcommerce1.vue")["default"]
28 - CardEcommerce2: typeof import("./src/components/cards/ecommerce/CardEcommerce2.vue")["default"]
29 - CardEcommerce3: typeof import("./src/components/cards/ecommerce/CardEcommerce3.vue")["default"]
30 - CardEcommerce4: typeof import("./src/components/cards/ecommerce/CardEcommerce4.vue")["default"]
31 - CardExtra1: typeof import("./src/components/cards/extra/CardExtra1.vue")["default"]
32 - CardExtra2: typeof import("./src/components/cards/extra/CardExtra2.vue")["default"]
33 - CardExtra3: typeof import("./src/components/cards/extra/CardExtra3.vue")["default"]
34 - CardExtra4: typeof import("./src/components/cards/extra/CardExtra4.vue")["default"]
35 - CardExtra5: typeof import("./src/components/cards/extra/CardExtra5.vue")["default"]
36 - CardExtra6: typeof import("./src/components/cards/extra/CardExtra6.vue")["default"]
37 - CardExtra7: typeof import("./src/components/cards/extra/CardExtra7.vue")["default"]
38 - CardSocial1: typeof import("./src/components/cards/social/CardSocial1.vue")["default"]
39 - CardWrapper: typeof import("./src/components/cards/CardWrapper.vue")["default"]
40 - RouterLink: typeof import("vue-router")["RouterLink"]
41 - RouterView: typeof import("vue-router")["RouterView"]
42 - }
8 +declare module 'vue' {
9 + export interface GlobalComponents {
10 + CardActions: typeof import('./src/components/cards/CardActions.vue')['default']
11 + CardBasic1: typeof import('./src/components/cards/basic/CardBasic1.vue')['default']
12 + CardBasic2: typeof import('./src/components/cards/basic/CardBasic2.vue')['default']
13 + CardBasic3: typeof import('./src/components/cards/basic/CardBasic3.vue')['default']
14 + CardBasic4: typeof import('./src/components/cards/basic/CardBasic4.vue')['default']
15 + CardBasic5: typeof import('./src/components/cards/basic/CardBasic5.vue')['default']
16 + CardBasic6: typeof import('./src/components/cards/basic/CardBasic6.vue')['default']
17 + CardCodeExample: typeof import('./src/components/cards/CardCodeExample.vue')['default']
18 + CardCombo1: typeof import('./src/components/cards/combo/CardCombo1.vue')['default']
19 + CardCombo2: typeof import('./src/components/cards/combo/CardCombo2.vue')['default']
20 + CardCombo3: typeof import('./src/components/cards/combo/CardCombo3.vue')['default']
21 + CardCombo4: typeof import('./src/components/cards/combo/CardCombo4.vue')['default']
22 + CardCombo5: typeof import('./src/components/cards/combo/CardCombo5.vue')['default']
23 + CardCombo6: typeof import('./src/components/cards/combo/CardCombo6.vue')['default']
24 + CardCombo7: typeof import('./src/components/cards/combo/CardCombo7.vue')['default']
25 + CardCombo8: typeof import('./src/components/cards/combo/CardCombo8.vue')['default']
26 + CardComboIcon: typeof import('./src/components/cards/combo/CardComboIcon.vue')['default']
27 + CardEcommerce1: typeof import('./src/components/cards/ecommerce/CardEcommerce1.vue')['default']
28 + CardEcommerce2: typeof import('./src/components/cards/ecommerce/CardEcommerce2.vue')['default']
29 + CardEcommerce3: typeof import('./src/components/cards/ecommerce/CardEcommerce3.vue')['default']
30 + CardEcommerce4: typeof import('./src/components/cards/ecommerce/CardEcommerce4.vue')['default']
31 + CardExtra1: typeof import('./src/components/cards/extra/CardExtra1.vue')['default']
32 + CardExtra2: typeof import('./src/components/cards/extra/CardExtra2.vue')['default']
33 + CardExtra3: typeof import('./src/components/cards/extra/CardExtra3.vue')['default']
34 + CardExtra4: typeof import('./src/components/cards/extra/CardExtra4.vue')['default']
35 + CardExtra5: typeof import('./src/components/cards/extra/CardExtra5.vue')['default']
36 + CardExtra6: typeof import('./src/components/cards/extra/CardExtra6.vue')['default']
37 + CardExtra7: typeof import('./src/components/cards/extra/CardExtra7.vue')['default']
38 + CardSocial1: typeof import('./src/components/cards/social/CardSocial1.vue')['default']
39 + CardWrapper: typeof import('./src/components/cards/CardWrapper.vue')['default']
40 + RouterLink: typeof import('vue-router')['RouterLink']
41 + RouterView: typeof import('vue-router')['RouterView']
42 + }
43 }