@nitra/vite-boot 2.3.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -2
- package/src/apollo.js +64 -37
- package/src/token.js +89 -48
- package/src/user.js +20 -0
- package/src/auto-login.js +0 -18
- package/src/get-token.js +0 -6
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/vite-boot",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Vite boot",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
|
-
"./auto-login": "./src/auto-login.js",
|
|
8
7
|
"./apollo": "./src/apollo.js",
|
|
9
8
|
"./pinia": "./src/pinia.js",
|
|
10
9
|
"./sentry": "./src/sentry.js",
|
package/src/apollo.js
CHANGED
|
@@ -2,72 +2,99 @@ import { ApolloClient, InMemoryCache, from } from '@apollo/client/core'
|
|
|
2
2
|
import { onError } from '@apollo/client/link/error'
|
|
3
3
|
import { createClient } from 'graphql-ws'
|
|
4
4
|
import { createLogger } from '@nitra/consola'
|
|
5
|
-
import { refreshToken } from './token.js'
|
|
6
5
|
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
|
|
7
|
-
import { getToken } from './
|
|
6
|
+
import { checkToken, refreshToken, getToken } from './token.js'
|
|
7
|
+
import { user } from './user.js'
|
|
8
|
+
import { router } from 'src/njs/boot/router.js'
|
|
8
9
|
|
|
9
10
|
const consola = createLogger(import.meta.url)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
function createRestartableClient(options) {
|
|
13
|
+
let restartRequested = false
|
|
14
|
+
let restart = () => {
|
|
15
|
+
restartRequested = true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const client = createClient({
|
|
19
|
+
...options,
|
|
20
|
+
on: {
|
|
21
|
+
...options.on,
|
|
22
|
+
opened: socket => {
|
|
23
|
+
options.on?.opened?.(socket)
|
|
24
|
+
|
|
25
|
+
restart = () => {
|
|
26
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
27
|
+
// if the socket is still open for the restart, do the restart
|
|
28
|
+
socket.close(4205, 'Client Restart')
|
|
29
|
+
} else {
|
|
30
|
+
// otherwise the socket might've closed, indicate that you want
|
|
31
|
+
// a restart on the next opened event
|
|
32
|
+
restartRequested = true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// just in case you were eager to restart
|
|
37
|
+
if (restartRequested) {
|
|
38
|
+
restartRequested = false
|
|
39
|
+
restart()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
...client,
|
|
47
|
+
restart: () => restart()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export let connectionParam = {}
|
|
52
|
+
export const wsClient = createRestartableClient({
|
|
12
53
|
url: `${import.meta.env.VITE_HASURA_PROTOCOL}://${import.meta.env.VITE_HASURA_HOST}/v1/graphql`,
|
|
13
|
-
disablePong: true,
|
|
14
54
|
connectionParams: () => {
|
|
15
55
|
const token = getToken()
|
|
16
56
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
57
|
+
if (token) {
|
|
58
|
+
const check = checkToken(token)
|
|
59
|
+
if (check.result === 'ok' && user.role) {
|
|
60
|
+
const headers = {}
|
|
61
|
+
// Якщо є токен - додаємо його в заголовки
|
|
62
|
+
headers.Authorization = `Bearer ${token}`
|
|
63
|
+
headers['x-hasura-role'] = user.role
|
|
64
|
+
|
|
65
|
+
connectionParam = { headers }
|
|
66
|
+
return connectionParam
|
|
21
67
|
}
|
|
22
68
|
}
|
|
69
|
+
|
|
70
|
+
connectionParam = {}
|
|
71
|
+
return connectionParam
|
|
23
72
|
}
|
|
24
73
|
})
|
|
25
74
|
|
|
26
75
|
const wsLink = new GraphQLWsLink(wsClient)
|
|
27
76
|
|
|
28
|
-
const errorLink = onError(({ graphQLErrors, networkError
|
|
77
|
+
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
|
29
78
|
if (graphQLErrors) {
|
|
30
79
|
for (const err of graphQLErrors) {
|
|
31
80
|
consola.debug('graphQLErrors: ', err)
|
|
32
|
-
|
|
33
|
-
// switch (err.extensions.code) {
|
|
34
|
-
// // Apollo Server sets code to UNAUTHENTICATED
|
|
35
|
-
// // when an AuthenticationError is thrown in a resolver
|
|
36
|
-
// case 'UNAUTHENTICATED':
|
|
37
|
-
// // Modify the operation context with a new token
|
|
38
|
-
// const oldHeaders = operation.getContext().headers
|
|
39
|
-
// operation.setContext({
|
|
40
|
-
// headers: {
|
|
41
|
-
// ...oldHeaders,
|
|
42
|
-
// authorization: getNewToken()
|
|
43
|
-
// }
|
|
44
|
-
// })
|
|
45
|
-
// // Retry the request, returning the new observable
|
|
46
|
-
// return forward(operation)
|
|
47
|
-
// }
|
|
48
81
|
}
|
|
49
82
|
}
|
|
50
83
|
|
|
51
|
-
// To retry on network errors, we recommend the RetryLink
|
|
52
|
-
// instead of the onError link. This just logs the error.
|
|
53
84
|
if (networkError) {
|
|
54
85
|
if (networkError?.message?.match('JWTExpired')) {
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// то видавати йому новий токен
|
|
58
|
-
// тобто ця перевірка універсальна
|
|
59
|
-
// не в залежності який типом авторизувався користувач
|
|
60
|
-
// а в залежності від того чи він валідний в БД
|
|
86
|
+
// Запускаємо рефреш токену асинхронно
|
|
87
|
+
// Чистимо токен
|
|
61
88
|
refreshToken()
|
|
62
|
-
} else if (networkError?.message
|
|
63
|
-
|
|
89
|
+
} else if (networkError?.message === `Invalid message 'type' property "connection_error"`) {
|
|
90
|
+
// Роль не підходить хашурі
|
|
91
|
+
router.push('/logout')
|
|
64
92
|
} else {
|
|
65
|
-
console.log(`[Network error]:
|
|
93
|
+
console.log(`[Network error]: `, networkError?.message, networkError)
|
|
66
94
|
}
|
|
67
95
|
}
|
|
68
96
|
})
|
|
69
97
|
|
|
70
|
-
// Create the apollo client
|
|
71
98
|
export const apolloClient = new ApolloClient({
|
|
72
99
|
link: from([errorLink, wsLink]),
|
|
73
100
|
cache: new InMemoryCache(),
|
package/src/token.js
CHANGED
|
@@ -1,61 +1,102 @@
|
|
|
1
|
-
import
|
|
1
|
+
import jwtDecode from '@nitra/jwt-decode'
|
|
2
|
+
import { unsetUser, setUser } from './user.js'
|
|
3
|
+
import { wsClient, apolloClient } from './apollo.js'
|
|
2
4
|
|
|
3
|
-
const
|
|
5
|
+
const allowedRoles = import.meta.env.VITE_HASURA_ROLE.split(',')
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
8
|
+
* Запам'ятовуємо поточну сторінку
|
|
9
|
+
* та перенаправляємо на сторінку входу
|
|
10
|
+
*
|
|
11
|
+
* @param {string} token - JWT токен
|
|
12
|
+
* @returns {{result: string, token: {}}} - Повертає результат перевірки токена
|
|
7
13
|
*/
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
export function checkToken(token) {
|
|
15
|
+
const decoded = jwtDecode(token)
|
|
16
|
+
|
|
17
|
+
if (!decoded) {
|
|
18
|
+
cleanToken()
|
|
19
|
+
return { result: 'cleaned' }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!decoded['https://hasura.io/jwt/claims']['x-hasura-allowed-roles'].some(x => allowedRoles.some(y => y === x))) {
|
|
23
|
+
// Виходимо
|
|
24
|
+
return { result: 'broken-role' }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const isExpired = !decoded || Math.floor(Date.now() / 1000) > decoded.exp
|
|
28
|
+
if (isExpired) {
|
|
29
|
+
refreshToken()
|
|
30
|
+
return { result: 'expired' }
|
|
31
|
+
}
|
|
11
32
|
|
|
12
|
-
|
|
33
|
+
return { result: 'ok', decoded }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Токен з кукі
|
|
38
|
+
*/
|
|
39
|
+
export function getToken() {
|
|
40
|
+
const cookieObj = new URLSearchParams(document.cookie.replaceAll('; ', '&')) // eslint-disable-line
|
|
41
|
+
const token = cookieObj.get('__session')
|
|
13
42
|
|
|
43
|
+
return token
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Переводимо в анонімного користувача
|
|
48
|
+
*/
|
|
49
|
+
export function cleanToken() {
|
|
50
|
+
unsetUser()
|
|
51
|
+
document.cookie = `__session=; Max-Age=0; path=/; domain=${import.meta.env.VITE_DOMAIN}` // eslint-disable-line
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function refreshToken() {
|
|
55
|
+
try {
|
|
56
|
+
const token = getToken()
|
|
57
|
+
|
|
58
|
+
// Чекаємо, можливо це паралельний JWTExpired
|
|
59
|
+
// виходимо якщо вже пішов рефреш токена від іншого запиту
|
|
14
60
|
if (!token) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (result
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
// Якщо ми на сторінці логіну або логауту
|
|
42
|
-
const awayFrom = ['/login', '/logout']
|
|
43
|
-
if (awayFrom.includes(window.location.pathname)) {
|
|
44
|
-
// то переводимо в корінь
|
|
45
|
-
window.location.replace('/')
|
|
46
|
-
} else {
|
|
47
|
-
window.location.reload()
|
|
48
|
-
}
|
|
49
|
-
} else {
|
|
50
|
-
// інакше вихід
|
|
51
|
-
window.location.replace('/logout')
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Прибираємо токен, щоб інші паралельні JWTExpired не виконувались
|
|
65
|
+
cleanToken()
|
|
66
|
+
|
|
67
|
+
const response = await fetch('/refresh-token', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: {
|
|
70
|
+
'Content-Type': 'application/json'
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({ token })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const result = await response.json()
|
|
76
|
+
|
|
77
|
+
// Якщо успішно отримали токен
|
|
78
|
+
// та запущений аполо
|
|
79
|
+
if (result.token && apolloClient) {
|
|
80
|
+
// successToken = true
|
|
81
|
+
const check = checkToken(result.token)
|
|
82
|
+
if (check.result === 'ok') {
|
|
83
|
+
setUser(check.decoded)
|
|
84
|
+
|
|
85
|
+
// перезавантажуємо щоб він вступив в дію
|
|
86
|
+
wsClient.restart()
|
|
52
87
|
}
|
|
53
88
|
}
|
|
54
89
|
} catch (error) {
|
|
55
90
|
console.error('Error:', error)
|
|
91
|
+
// } finally {
|
|
92
|
+
// // Якщо невдача отримання токену
|
|
93
|
+
// if (!successToken) {
|
|
94
|
+
// // Якщо користувач не анонімний зараз
|
|
95
|
+
// // TODO: допрацювати
|
|
96
|
+
// if (Object.keys(connectionParam).length === 0) {
|
|
97
|
+
// // перезавантажуємо щоб він вступив в дію як анонімний
|
|
98
|
+
// wsClient.restart()
|
|
99
|
+
// }
|
|
100
|
+
// }
|
|
56
101
|
}
|
|
57
102
|
}
|
|
58
|
-
|
|
59
|
-
function sleep(ms) {
|
|
60
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
61
|
-
}
|
package/src/user.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const allowedRoles = import.meta.env.VITE_HASURA_ROLE.split(',')
|
|
2
|
+
|
|
3
|
+
export let user = {}
|
|
4
|
+
|
|
5
|
+
export function unsetUser() {
|
|
6
|
+
user = {}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function setUser(u) {
|
|
10
|
+
user = u
|
|
11
|
+
|
|
12
|
+
// Роль за умовчанням
|
|
13
|
+
const intersect = intersection(allowedRoles, u['https://hasura.io/jwt/claims']['x-hasura-allowed-roles'])
|
|
14
|
+
user.role = intersect[0]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function intersection(a, b) {
|
|
18
|
+
const setA = new Set(a)
|
|
19
|
+
return b.filter(value => setA.has(value))
|
|
20
|
+
}
|
package/src/auto-login.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { refreshToken } from './token.js'
|
|
2
|
-
import jwtDecode from '@nitra/jwt-decode'
|
|
3
|
-
|
|
4
|
-
const token = localStorage.getItem('token')
|
|
5
|
-
|
|
6
|
-
if (token) {
|
|
7
|
-
const tokenDecoded = jwtDecode(token)
|
|
8
|
-
|
|
9
|
-
if (
|
|
10
|
-
!tokenDecoded['https://hasura.io/jwt/claims']['x-hasura-allowed-roles'].includes(import.meta.env.VITE_HASURA_ROLE)
|
|
11
|
-
) {
|
|
12
|
-
// Не дозволена роль поточного користувача в поточному прикладенні
|
|
13
|
-
window.location.replace('/logout')
|
|
14
|
-
} else if (tokenDecoded.exp < Math.floor(Date.now() / 1000)) {
|
|
15
|
-
// Перевіряємо чи не старий токен знаходиться в localStorage
|
|
16
|
-
await refreshToken()
|
|
17
|
-
}
|
|
18
|
-
}
|