@nitra/vite-boot 2.2.0 → 3.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 CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@nitra/vite-boot",
3
- "version": "2.2.0",
3
+ "version": "3.0.0",
4
4
  "description": "Vite boot",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  "./auto-login": "./src/auto-login.js",
8
8
  "./apollo": "./src/apollo.js",
9
- "./i18n": "./src/i18n.js",
10
9
  "./pinia": "./src/pinia.js",
11
10
  "./sentry": "./src/sentry.js",
12
11
  "./token": "./src/token.js"
@@ -46,8 +45,7 @@
46
45
  "@sentry/tracing": "^6.16.1",
47
46
  "@sentry/vue": "^6.16.1",
48
47
  "graphql-ws": "^5.5.5",
49
- "petite-vue-i18n": "^9.2.0-beta.26",
50
- "pinia": "^2.0.9",
51
- "pinia-plugin-persistedstate": "^1.0.3"
48
+ "pinia": "^2.0.28",
49
+ "pinia-plugin-persistedstate": "^3.0.1"
52
50
  }
53
51
  }
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 './get-token.js'
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
- const wsClient = createClient({
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
- return {
18
- headers: {
19
- Authorization: `Bearer ${token}`,
20
- 'x-hasura-role': import.meta.env.VITE_HASURA_ROLE
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, _operation, _forward }) => {
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?.match('role is not in allowed roles')) {
63
- window.location.replace('/logout')
89
+ } else if (networkError?.message === `Invalid message 'type' property "connection_error"`) {
90
+ // Роль не підходить хашурі
91
+ router.push('/logout')
64
92
  } else {
65
- console.log(`[Network error]: ${networkError}`)
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 { createLogger } from '@nitra/consola'
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 consola = createLogger(import.meta.url)
5
+ const allowedRoles = import.meta.env.VITE_HASURA_ROLE.split(',')
4
6
 
5
7
  /**
6
- * Ініціалізація Sentry
8
+ * Запам'ятовуємо поточну сторінку
9
+ * та перенаправляємо на сторінку входу
10
+ *
11
+ * @param {string} token - JWT токен
12
+ * @returns {{result: string, token: {}}} - Повертає результат перевірки токена
7
13
  */
8
- export const refreshToken = async () => {
9
- try {
10
- const token = localStorage.getItem('token')
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
- consola.debug('token refresh', token)
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
- // Чекаємо, можливо це паралельний JWTExpired
16
- consola.debug('begin wait 5 sec')
17
- await sleep(5000)
18
- consola.debug('end wait, start logout')
19
- window.location.replace('/logout')
20
- } else {
21
- // Прибираємо токен, щоб інші паралельні JWTExpired не виконувались
22
- localStorage.removeItem('token')
23
- consola.debug('token cleaned')
24
-
25
- const response = await fetch('/refresh-token', {
26
- credentials: 'include',
27
- method: 'POST',
28
- headers: {
29
- 'Content-Type': 'application/json'
30
- },
31
- body: JSON.stringify({ token })
32
- })
33
- const result = await response.json()
34
- consola.debug('result refresh', result)
35
-
36
- if (result.token) {
37
- // якщо токен прийшов - замінюємо його
38
- localStorage.setItem('token', result.token)
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
- }
package/src/get-token.js DELETED
@@ -1,6 +0,0 @@
1
- export const getToken = () => {
2
- const cookieObj = new URLSearchParams(document.cookie.replaceAll('; ', '&')) // eslint-disable-line
3
- const token = cookieObj.get('jwt-auth')
4
-
5
- return token
6
- }
package/src/i18n.js DELETED
@@ -1,41 +0,0 @@
1
- import { createI18n } from 'petite-vue-i18n'
2
- import { persistStore } from 'src/stores/persist.js'
3
- import { createLogger } from '@nitra/consola'
4
-
5
- const consola = createLogger(import.meta.url)
6
-
7
- export const i18n = createI18n({
8
- locale: 'ru',
9
- fallbackLocale: 'ru',
10
- legacy: false
11
- })
12
-
13
- /**
14
- * Переводимо з варіанту:
15
- * { "индекс": "індекс" }
16
- * в
17
- * {
18
- * "ru": { "индекс": "индекс" },
19
- * "uk": { "индекс": "індекс" }
20
- * }
21
- *
22
- * @param {Object} uk
23
- * @returns {Object}
24
- */
25
- export const mt = uk => {
26
- const ukP = { uk: uk }
27
-
28
- ukP.ru = Object.assign({}, ...Object.keys(uk).map(key => ({ [key]: key })))
29
-
30
- return ukP
31
- }
32
-
33
- export const updGlobalLocale = lang => {
34
- const persist = persistStore()
35
- if (lang) {
36
- persist.locale = lang
37
- }
38
- i18n.global.locale.value = persist.locale
39
- }
40
-
41
- consola.debug('End i18n boot')