@nitra/vite-boot 4.1.3 → 5.0.0
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 +3 -4
- package/src/apollo.js +55 -37
- package/src/router.js +2 -2
- package/src/token.js +42 -41
- package/src/user.js +27 -5
- package/src/apollo-v4-compat.js +0 -20
- package/src/login.js +0 -39
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/vite-boot",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Vite boot",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./apollo": "./src/apollo.js",
|
|
8
8
|
"./pinia": "./src/pinia.js",
|
|
9
9
|
"./token": "./src/token.js",
|
|
10
|
-
"./login": "./src/login.js",
|
|
11
10
|
"./user": "./src/user.js",
|
|
12
11
|
"./router": "./src/router.js"
|
|
13
12
|
},
|
|
@@ -26,9 +25,9 @@
|
|
|
26
25
|
"src"
|
|
27
26
|
],
|
|
28
27
|
"dependencies": {
|
|
29
|
-
"@apollo/client": "^4.0.
|
|
28
|
+
"@apollo/client": "^4.0.7",
|
|
30
29
|
"@nitra/jwt-decode": "^1.2.0",
|
|
31
|
-
"@
|
|
30
|
+
"@vue3-apollo/core": "^1.3.1",
|
|
32
31
|
"graphql-ws": "^6.0.6",
|
|
33
32
|
"pinia": "^3.0.3",
|
|
34
33
|
"pinia-plugin-persistedstate": "^4.5.0",
|
package/src/apollo.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import { ApolloClient,
|
|
2
|
-
import { setContext } from '@apollo/client/link/context'
|
|
1
|
+
import { ApolloClient, ApolloLink, HttpLink, InMemoryCache } from '@apollo/client/core'
|
|
3
2
|
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
|
|
4
3
|
import { getMainDefinition } from '@apollo/client/utilities'
|
|
5
4
|
import { createClient } from 'graphql-ws'
|
|
6
|
-
import {
|
|
7
|
-
import { user } from './user.js'
|
|
8
|
-
|
|
5
|
+
import { checkedToken, cleanToken, user } from './user.js'
|
|
9
6
|
export { gql } from '@apollo/client/core'
|
|
10
|
-
export {
|
|
7
|
+
export { apolloPlugin, useApolloClient, useMutation, useQuery, useSubscription } from '@vue3-apollo/core'
|
|
8
|
+
|
|
9
|
+
function getAuthHeaders() {
|
|
10
|
+
// Якщо є токен - додаємо його в заголовки
|
|
11
|
+
if (checkedToken) {
|
|
12
|
+
const headers = {}
|
|
13
|
+
|
|
14
|
+
headers.Authorization = `Bearer ${checkedToken}`
|
|
15
|
+
headers['x-hasura-role'] = user.role
|
|
16
|
+
|
|
17
|
+
return headers
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {}
|
|
21
|
+
}
|
|
11
22
|
|
|
12
|
-
const httpLink =
|
|
23
|
+
const httpLink = new HttpLink({
|
|
13
24
|
uri: `${import.meta.env.VITE_HASURA_PROTOCOL === 'wss' ? 'https' : 'http'}://${
|
|
14
25
|
import.meta.env.VITE_HASURA_HOST
|
|
15
26
|
}/v1/graphql`
|
|
@@ -20,6 +31,9 @@ const httpLink = createHttpLink({
|
|
|
20
31
|
* @returns {void}
|
|
21
32
|
*/
|
|
22
33
|
export function logout() {
|
|
34
|
+
cleanToken()
|
|
35
|
+
// Очищаємо Apollo Client кеш і закриваємо зʼєднання
|
|
36
|
+
resetApolloClient()
|
|
23
37
|
const logoutUrl = `${import.meta.env.BASE_URL}logout`
|
|
24
38
|
// Якщо не на сторінці логауту, то перенаправляємо на сторінку логауту
|
|
25
39
|
if (globalThis.location.pathname !== logoutUrl) {
|
|
@@ -32,47 +46,34 @@ export function logout() {
|
|
|
32
46
|
* @param {Record<string, string>} headers - Початкові заголовки запиту
|
|
33
47
|
* @returns {{headers: Record<string, string>}} - Обʼєкт з підготовленими заголовками
|
|
34
48
|
*/
|
|
35
|
-
function prepareHeaders(headers = {}) {
|
|
36
|
-
const token = getToken()
|
|
37
|
-
|
|
38
|
-
if (!token) {
|
|
39
|
-
logout()
|
|
40
|
-
}
|
|
41
|
-
const check = checkToken(token)
|
|
42
|
-
|
|
43
|
-
// Якщо роль не підходить, то перенаправляємо на сторінку логауту
|
|
44
|
-
if (check.result === 'broken-role') {
|
|
45
|
-
logout()
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (check.result !== 'ok' || !user.role) {
|
|
49
|
-
logout()
|
|
50
|
-
}
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
// Додаємо токен і роль до кожного HTTP-запиту за допомогою ApolloLink
|
|
51
|
+
const authLink = new ApolloLink((operation, forward) => {
|
|
52
|
+
const existingHeaders = operation.getContext().headers || {}
|
|
53
|
+
operation.setContext({
|
|
54
|
+
headers: {
|
|
55
|
+
...existingHeaders,
|
|
56
|
+
...getAuthHeaders()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
return forward(operation)
|
|
61
60
|
})
|
|
62
61
|
|
|
63
62
|
export const wsClient = createClient({
|
|
64
63
|
url: `${import.meta.env.VITE_HASURA_PROTOCOL}://${import.meta.env.VITE_HASURA_HOST}/v1/graphql`,
|
|
65
|
-
connectionParams: () => {
|
|
66
|
-
|
|
67
|
-
},
|
|
64
|
+
connectionParams: () => ({
|
|
65
|
+
headers: getAuthHeaders()
|
|
66
|
+
}),
|
|
68
67
|
// Обробник успішного підключення
|
|
69
68
|
on: {
|
|
70
69
|
// Обробник помилок підключення
|
|
70
|
+
/** @param {unknown} error - Помилка WebSocket з'єднання */
|
|
71
71
|
error: error => {
|
|
72
72
|
console.error('WebSocket помилка:', error)
|
|
73
73
|
},
|
|
74
74
|
|
|
75
75
|
// Обробник закриття з'єднання
|
|
76
|
+
/** @param {{ code: number, reason: string }} event - Подія закриття WebSocket */
|
|
76
77
|
closed: event => {
|
|
77
78
|
console.info('WebSocket закрито:', event.code, event.reason)
|
|
78
79
|
|
|
@@ -93,13 +94,13 @@ export const wsClient = createClient({
|
|
|
93
94
|
const wsLink = new GraphQLWsLink(wsClient)
|
|
94
95
|
|
|
95
96
|
// Розділяємо трафік: subscription -> WS, інше -> HTTP
|
|
96
|
-
const link = split(
|
|
97
|
+
const link = ApolloLink.split(
|
|
97
98
|
({ query }) => {
|
|
98
99
|
const def = getMainDefinition(query)
|
|
99
100
|
return def.kind === 'OperationDefinition' && def.operation === 'subscription'
|
|
100
101
|
},
|
|
101
102
|
wsLink,
|
|
102
|
-
from([authLink, httpLink])
|
|
103
|
+
ApolloLink.from([authLink, httpLink])
|
|
103
104
|
)
|
|
104
105
|
|
|
105
106
|
// Ініціалізуємо клієнт
|
|
@@ -107,3 +108,20 @@ export const apolloClient = new ApolloClient({
|
|
|
107
108
|
link,
|
|
108
109
|
cache: new InMemoryCache()
|
|
109
110
|
})
|
|
111
|
+
/**
|
|
112
|
+
* Повне скидання Apollo Client (для критичних випадків)
|
|
113
|
+
*/
|
|
114
|
+
export function resetApolloClient() {
|
|
115
|
+
if (apolloClient) {
|
|
116
|
+
// Очищаємо кеш
|
|
117
|
+
apolloClient.clearStore()
|
|
118
|
+
|
|
119
|
+
// Закриваємо всі активні запити
|
|
120
|
+
apolloClient.stop()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Закриваємо WebSocket з'єднання
|
|
124
|
+
if (wsClient && wsClient.dispose) {
|
|
125
|
+
wsClient.dispose()
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/router.js
CHANGED
|
@@ -36,7 +36,7 @@ export function canUserAccess(to) {
|
|
|
36
36
|
if (!user.role) {
|
|
37
37
|
const check = checkToken(token)
|
|
38
38
|
if (check.result === 'ok') {
|
|
39
|
-
setUser(check.decoded)
|
|
39
|
+
setUser(check.decoded, token)
|
|
40
40
|
} else {
|
|
41
41
|
return false
|
|
42
42
|
}
|
|
@@ -76,7 +76,7 @@ export function canUserAccessMetaRoles(to) {
|
|
|
76
76
|
if (!user.role) {
|
|
77
77
|
const check = checkToken(token)
|
|
78
78
|
if (check.result === 'ok') {
|
|
79
|
-
setUser(check.decoded)
|
|
79
|
+
setUser(check.decoded, token)
|
|
80
80
|
} else {
|
|
81
81
|
return false
|
|
82
82
|
}
|
package/src/token.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import jwtDecode from '@nitra/jwt-decode'
|
|
2
|
-
import {
|
|
3
|
-
import { defaultLogin } from './login.js'
|
|
4
|
-
import { setUser, unsetUser } from './user.js'
|
|
2
|
+
import { cleanToken, setUser, unsetUser } from './user.js'
|
|
5
3
|
|
|
6
4
|
// @ts-ignore
|
|
7
5
|
const allowedRoles = import.meta.env.VITE_HASURA_ROLE.split(',')
|
|
@@ -12,6 +10,7 @@ const allowedRoles = import.meta.env.VITE_HASURA_ROLE.split(',')
|
|
|
12
10
|
* @returns {{result: 'ok'|'expired'|'cleaned'|'broken-role', decoded?: object}} - Результат перевірки
|
|
13
11
|
*/
|
|
14
12
|
export function checkToken(token) {
|
|
13
|
+
/** @type {{ exp?: number, [k: string]: any }} */
|
|
15
14
|
const decoded = jwtDecode(token)
|
|
16
15
|
if (!decoded || !decoded['https://hasura.io/jwt/claims']) {
|
|
17
16
|
cleanToken()
|
|
@@ -19,67 +18,69 @@ export function checkToken(token) {
|
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
if (!decoded['https://hasura.io/jwt/claims']['x-hasura-allowed-roles'].some(x => allowedRoles.includes(x))) {
|
|
21
|
+
unsetUser()
|
|
22
22
|
// Виходимо
|
|
23
23
|
return { result: 'broken-role' }
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const isExpired = !decoded || Math.floor(Date.now() / 1000) > decoded.exp
|
|
27
27
|
if (isExpired) {
|
|
28
|
+
unsetUser()
|
|
28
29
|
refreshToken()
|
|
29
30
|
return { result: 'expired' }
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
setUser(decoded, token)
|
|
32
34
|
return { result: 'ok', decoded }
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
|
-
*
|
|
37
|
-
* @
|
|
38
|
+
* Вхід або на попередню або на кореневу сторінку
|
|
39
|
+
* @param {object} decoded - Розкодований JWT з Hasura claims
|
|
40
|
+
* @param {string} raw - JWT як рядок (для localStorage)
|
|
41
|
+
* @returns {void}
|
|
38
42
|
*/
|
|
39
|
-
export function
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
const token = cookieObj.get('__session')
|
|
43
|
-
if (token) {
|
|
44
|
-
return token
|
|
45
|
-
}
|
|
43
|
+
export function defaultLogin(decoded, raw) {
|
|
44
|
+
// Якщо задано що токен з localStorage
|
|
45
|
+
localStorage.setItem('__session', raw)
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
setUser(decoded, raw)
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
const route = sessionStorage.getItem('url-before-logout') || import.meta.env.BASE_URL
|
|
50
|
+
globalThis.location.href = route // router.push(route)
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
/**
|
|
52
|
-
*
|
|
54
|
+
* Автоматичний вхід користувача
|
|
55
|
+
* @returns {Promise<void>}
|
|
53
56
|
*/
|
|
54
|
-
export function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
export function autoLogin() {
|
|
58
|
+
const token = getToken()
|
|
59
|
+
if (!token) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
const check = checkToken(token)
|
|
64
|
+
if (check.result !== 'ok') {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
document.cookie = `__session=; Max-Age=0; path=/; domain=${import.meta.env.VITE_DOMAIN}`
|
|
68
|
+
defaultLogin(check.decoded, token)
|
|
65
69
|
}
|
|
66
|
-
|
|
67
70
|
/**
|
|
68
|
-
*
|
|
71
|
+
* Токен з кукі
|
|
72
|
+
* @returns {string} токен
|
|
69
73
|
*/
|
|
70
|
-
export function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
apolloClient.stop()
|
|
74
|
+
export function getToken() {
|
|
75
|
+
// інакше з кукі
|
|
76
|
+
const cookieObj = new URLSearchParams(document.cookie.replaceAll('; ', '&'))
|
|
77
|
+
const token = cookieObj.get('__session')
|
|
78
|
+
if (token) {
|
|
79
|
+
return token
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
wsClient.dispose()
|
|
82
|
-
}
|
|
82
|
+
// Якщо задано що токен беремо з localStorage
|
|
83
|
+
return localStorage.getItem('__session')
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
/**
|
|
@@ -117,14 +118,14 @@ export async function refreshToken() {
|
|
|
117
118
|
|
|
118
119
|
// Якщо запущений аполо
|
|
119
120
|
// допускаємо що ми всередині прикладення
|
|
120
|
-
if (globalThis.location.pathname
|
|
121
|
+
if (globalThis.location.pathname === '/login') {
|
|
122
|
+
await defaultLogin(check.decoded, result.token)
|
|
123
|
+
} else {
|
|
121
124
|
setUser(check.decoded)
|
|
122
125
|
|
|
123
126
|
// перезавантажуємо щоб він вступив в дію
|
|
124
127
|
// wsClient.restart()
|
|
125
|
-
wsClient.dispose()
|
|
126
|
-
} else {
|
|
127
|
-
await defaultLogin(check.decoded, result.token)
|
|
128
|
+
// wsClient.dispose()
|
|
128
129
|
}
|
|
129
130
|
} catch (error) {
|
|
130
131
|
console.error('Error:', error)
|
package/src/user.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
const allowedRoles = import.meta.env.VITE_HASURA_ROLE.split(',')
|
|
2
2
|
|
|
3
|
+
// Декодована версія токена, яка пройшла перевірку, на те що вона:
|
|
4
|
+
// - має /hasura.io/jwt/claims
|
|
5
|
+
// - має дозволені ролі
|
|
6
|
+
// - не застарів
|
|
7
|
+
export let checkedToken
|
|
8
|
+
|
|
3
9
|
export let user = {}
|
|
4
10
|
|
|
5
11
|
/**
|
|
@@ -7,18 +13,21 @@ export let user = {}
|
|
|
7
13
|
*/
|
|
8
14
|
export function unsetUser() {
|
|
9
15
|
user = {}
|
|
16
|
+
checkedToken = null
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* Встановлює дані користувача з розкодованого JWT
|
|
14
|
-
* @param {object}
|
|
21
|
+
* @param {object} decoded - Обʼєкт з Hasura claims розкодованого токена
|
|
22
|
+
* @param {String} raw - Raw токен
|
|
15
23
|
*/
|
|
16
|
-
export function setUser(
|
|
17
|
-
user =
|
|
24
|
+
export function setUser(decoded, raw = null) {
|
|
25
|
+
user = decoded
|
|
26
|
+
checkedToken = raw
|
|
18
27
|
|
|
19
|
-
user.login =
|
|
28
|
+
user.login = decoded['https://hasura.io/jwt/claims']['x-hasura-user-id']
|
|
20
29
|
// Роль за умовчанням
|
|
21
|
-
const intersect = intersection(allowedRoles,
|
|
30
|
+
const intersect = intersection(allowedRoles, decoded['https://hasura.io/jwt/claims']['x-hasura-allowed-roles'])
|
|
22
31
|
user.role = intersect[0]
|
|
23
32
|
}
|
|
24
33
|
|
|
@@ -32,3 +41,16 @@ function intersection(a, b) {
|
|
|
32
41
|
const setA = new Set(a)
|
|
33
42
|
return b.filter(value => setA.has(value))
|
|
34
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Переводимо в анонімного користувача
|
|
47
|
+
*/
|
|
48
|
+
export function cleanToken() {
|
|
49
|
+
unsetUser()
|
|
50
|
+
|
|
51
|
+
// видаляємо з localStorage
|
|
52
|
+
localStorage.removeItem('__session')
|
|
53
|
+
|
|
54
|
+
// @ts-ignore
|
|
55
|
+
document.cookie = `__session=; Max-Age=0; path=/; domain=${import.meta.env.VITE_DOMAIN}`
|
|
56
|
+
}
|
package/src/apollo-v4-compat.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// Apollo Client v4 compatibility shim for @vue/apollo-composable
|
|
2
|
-
export class ApolloError extends Error {
|
|
3
|
-
constructor(opts = {}) {
|
|
4
|
-
super(opts.errorMessage || 'Apollo Client Error')
|
|
5
|
-
this.name = 'ApolloError'
|
|
6
|
-
this.graphQLErrors = opts.graphQLErrors ?? []
|
|
7
|
-
this.clientErrors = opts.clientErrors ?? []
|
|
8
|
-
this.networkError = opts.networkError
|
|
9
|
-
this.extraInfo = opts.extraInfo
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
*
|
|
15
|
-
* @param err
|
|
16
|
-
*/
|
|
17
|
-
export function isApolloError(err) {
|
|
18
|
-
return err instanceof ApolloError
|
|
19
|
-
}
|
|
20
|
-
// export * from '@apollo/client/core' // реекспорт всього
|
package/src/login.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { checkToken, getToken } from './token.js'
|
|
2
|
-
import { setUser } from './user.js'
|
|
3
|
-
// import { router } from './router.js'
|
|
4
|
-
// Використовуємо глобальну версію
|
|
5
|
-
// import { router } from '@nitra/vite-boot/router'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Автоматичний вхід користувача
|
|
9
|
-
* @returns {Promise<void>}
|
|
10
|
-
*/
|
|
11
|
-
export function autoLogin() {
|
|
12
|
-
const token = getToken()
|
|
13
|
-
if (!token) {
|
|
14
|
-
return
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const check = checkToken(token)
|
|
18
|
-
if (check.result !== 'ok') {
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
defaultLogin(check.decoded, token)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Вхід або на попередню або на кореневу сторінку
|
|
27
|
-
* @param {object} decoded - Розкодований JWT з Hasura claims
|
|
28
|
-
* @param {string} raw - JWT як рядок (для localStorage)
|
|
29
|
-
* @returns {void}
|
|
30
|
-
*/
|
|
31
|
-
export function defaultLogin(decoded, raw) {
|
|
32
|
-
// Якщо задано що токен з localStorage
|
|
33
|
-
localStorage.setItem('__session', raw)
|
|
34
|
-
|
|
35
|
-
setUser(decoded)
|
|
36
|
-
// @ts-ignore
|
|
37
|
-
const route = sessionStorage.getItem('url-before-logout') || import.meta.env.BASE_URL
|
|
38
|
-
globalThis.location.href = route // router.push(route)
|
|
39
|
-
}
|