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
}