nayota-show-sdk 1.3.95 → 1.3.96

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/config/urlcfg.js CHANGED
@@ -2,9 +2,10 @@ import { parse, serialize } from 'cookie-es'
2
2
 
3
3
  const config = {
4
4
  authTokenName: 'Admin-Token',
5
+ refreshTokenName: 'Refresh-Token',
5
6
  version: 'v1',
6
7
  getAuthToken: function() {
7
- return parse(document.cookie)[this.authTokenName]
8
+ return parse(document.cookie || '')[this.authTokenName] || localStorage.getItem(this.authTokenName)
8
9
  },
9
10
  setAuthToken: function(token) {
10
11
  document.cookie = serialize(this.authTokenName, token, {
@@ -15,6 +16,37 @@ const config = {
15
16
  })
16
17
  localStorage.setItem(this.authTokenName, token)
17
18
  },
19
+ clearAuthToken: function() {
20
+ document.cookie = serialize(this.authTokenName, '', {
21
+ path: '/',
22
+ maxAge: 0,
23
+ sameSite: 'strict'
24
+ })
25
+ localStorage.removeItem(this.authTokenName)
26
+ },
27
+ getRefreshToken: function() {
28
+ return parse(document.cookie || '')[this.refreshTokenName] || localStorage.getItem(this.refreshTokenName)
29
+ },
30
+ setRefreshToken: function(token) {
31
+ document.cookie = serialize(this.refreshTokenName, token, {
32
+ path: '/',
33
+ maxAge: 86400,
34
+ sameSite: 'strict'
35
+ })
36
+ localStorage.setItem(this.refreshTokenName, token)
37
+ },
38
+ clearRefreshToken: function() {
39
+ document.cookie = serialize(this.refreshTokenName, '', {
40
+ path: '/',
41
+ maxAge: 0,
42
+ sameSite: 'strict'
43
+ })
44
+ localStorage.removeItem(this.refreshTokenName)
45
+ },
46
+ clearTokens: function() {
47
+ this.clearAuthToken()
48
+ this.clearRefreshToken()
49
+ },
18
50
  getVersion() {
19
51
  return this.version || 'v1'
20
52
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nayota-show-sdk",
3
- "version": "1.3.95",
3
+ "version": "1.3.96",
4
4
  "description": "nayota-show-server rest-api",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -0,0 +1,192 @@
1
+ import axios from 'axios'
2
+
3
+ import urlcfg from '../config/urlcfg'
4
+
5
+ const API_PREFIX = '/api/v1'
6
+ const AUTH_PATH_REGEXP = /\/auth\/(login|login-options|refresh|logout)(\?.*)?$/
7
+ const GLOBAL_REFRESH_KEY = '__NAYOTA_V2_AUTH_REFRESH_PROMISE__'
8
+
9
+ let refreshPromise = null
10
+
11
+ function getSharedRefreshPromise() {
12
+ if (typeof globalThis === 'undefined') {
13
+ return refreshPromise
14
+ }
15
+
16
+ return globalThis[GLOBAL_REFRESH_KEY] || null
17
+ }
18
+
19
+ function setSharedRefreshPromise(promise) {
20
+ refreshPromise = promise
21
+ if (typeof globalThis !== 'undefined') {
22
+ globalThis[GLOBAL_REFRESH_KEY] = promise
23
+ }
24
+ }
25
+
26
+ function getApiBaseUrl() {
27
+ const rawBaseUrl = urlcfg.getIotUrl() || urlcfg.getShowUrl() || ''
28
+
29
+ return rawBaseUrl.replace(/\/api(?:-v1|\/v1)?\/?$/, '')
30
+ }
31
+
32
+ export function buildV2ApiUrl(path) {
33
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`
34
+
35
+ return `${getApiBaseUrl()}${API_PREFIX}${normalizedPath}`
36
+ }
37
+
38
+ export function shouldUseV2Refresh() {
39
+ return urlcfg.isV2()
40
+ }
41
+
42
+ export function shouldSkipV2Refresh(requestConfig = {}) {
43
+ const requestUrl = requestConfig.url || ''
44
+
45
+ return requestConfig._retry || AUTH_PATH_REGEXP.test(requestUrl)
46
+ }
47
+
48
+ function getRefreshPayload(responseData = {}) {
49
+ return responseData?.data && typeof responseData.data === 'object'
50
+ ? responseData.data
51
+ : responseData
52
+ }
53
+
54
+ function parseJwtPayload(token) {
55
+ if (!token || typeof token !== 'string') {
56
+ return null
57
+ }
58
+
59
+ const [, payload] = token.split('.')
60
+ if (!payload) {
61
+ return null
62
+ }
63
+
64
+ try {
65
+ const normalized = payload.replace(/-/g, '+').replace(/_/g, '/')
66
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')
67
+ const decoded = typeof atob === 'function'
68
+ ? atob(padded)
69
+ : Buffer.from(padded, 'base64').toString('binary')
70
+ const json = decodeURIComponent(
71
+ decoded
72
+ .split('')
73
+ .map(char => `%${(`00${char.charCodeAt(0).toString(16)}`).slice(-2)}`)
74
+ .join('')
75
+ )
76
+
77
+ return JSON.parse(json)
78
+ } catch {
79
+ return null
80
+ }
81
+ }
82
+
83
+ function parseStoredJson(value) {
84
+ if (!value || typeof value !== 'string') {
85
+ return null
86
+ }
87
+
88
+ try {
89
+ return JSON.parse(decodeURIComponent(value))
90
+ } catch {
91
+ try {
92
+ return JSON.parse(value)
93
+ } catch {
94
+ return null
95
+ }
96
+ }
97
+ }
98
+
99
+ function getCookieValue(name) {
100
+ if (typeof document === 'undefined' || !name) {
101
+ return null
102
+ }
103
+
104
+ const prefix = `${name}=`
105
+ const cookie = (document.cookie || '')
106
+ .split(';')
107
+ .map(item => item.trim())
108
+ .find(item => item.startsWith(prefix))
109
+
110
+ return cookie ? cookie.slice(prefix.length) : null
111
+ }
112
+
113
+ function getStoredSessionUserId() {
114
+ if (typeof localStorage !== 'undefined') {
115
+ const sessionUserId = localStorage.getItem('sessionUserId')
116
+ if (sessionUserId) {
117
+ return sessionUserId
118
+ }
119
+ }
120
+
121
+ const userData = parseStoredJson(getCookieValue('userData'))
122
+ return userData?._id || userData?.id || null
123
+ }
124
+
125
+ function getUserId(user = {}) {
126
+ return user?.id || user?._id || null
127
+ }
128
+
129
+ function assertAccessTokenOwner(accessToken, user) {
130
+ const tokenUserId = parseJwtPayload(accessToken)?.sub
131
+ const expectedUserId = getUserId(user) || getStoredSessionUserId()
132
+
133
+ if (tokenUserId && expectedUserId && tokenUserId !== expectedUserId) {
134
+ urlcfg.clearTokens()
135
+ throw {
136
+ code: 401,
137
+ message: 'accessToken 用户不匹配,已清除本地认证状态'
138
+ }
139
+ }
140
+ }
141
+
142
+ export async function refreshV2AccessToken() {
143
+ const refreshToken = urlcfg.getRefreshToken()
144
+
145
+ if (!refreshToken) {
146
+ urlcfg.clearTokens()
147
+ throw {
148
+ code: 401,
149
+ message: 'refreshToken 不存在'
150
+ }
151
+ }
152
+
153
+ if (!getSharedRefreshPromise()) {
154
+ setSharedRefreshPromise(axios({
155
+ url: buildV2ApiUrl('/auth/refresh'),
156
+ method: 'post',
157
+ data: { refreshToken },
158
+ headers: {
159
+ 'Content-Type': 'application/json;charset=UTF-8'
160
+ },
161
+ withCredentials: true,
162
+ timeout: 30000
163
+ })
164
+ .then(response => {
165
+ const payload = getRefreshPayload(response?.data)
166
+ const accessToken = payload?.accessToken
167
+
168
+ if (!accessToken) {
169
+ throw {
170
+ code: 401,
171
+ message: '刷新 accessToken 失败'
172
+ }
173
+ }
174
+
175
+ assertAccessTokenOwner(accessToken, payload?.user)
176
+ urlcfg.setAuthToken(accessToken)
177
+ if (payload?.refreshToken) {
178
+ urlcfg.setRefreshToken(payload.refreshToken)
179
+ }
180
+ return accessToken
181
+ })
182
+ .catch(error => {
183
+ urlcfg.clearTokens()
184
+ throw error
185
+ })
186
+ .finally(() => {
187
+ setSharedRefreshPromise(null)
188
+ }))
189
+ }
190
+
191
+ return getSharedRefreshPromise()
192
+ }
@@ -3,6 +3,7 @@ import axios from 'axios'
3
3
 
4
4
  import urlcfg from '../config/urlcfg'
5
5
  import emitter from './EventEmitter'
6
+ import { refreshV2AccessToken, shouldSkipV2Refresh, shouldUseV2Refresh } from './http-auth'
6
7
 
7
8
  function isSuccessStatus(status) {
8
9
  return typeof status === 'number' && status >= 200 && status < 300
@@ -41,7 +42,7 @@ function createHttpInstance({
41
42
  http.interceptors.response.use(
42
43
  res => {
43
44
  const newToken = res.headers['x-new-token']
44
- if (newToken) {
45
+ if (newToken && !shouldUseV2Refresh()) {
45
46
  urlcfg.setAuthToken(newToken)
46
47
  axios.defaults.headers.common.Authorization = `Bearer ${newToken}`
47
48
  }
@@ -72,7 +73,26 @@ function createHttpInstance({
72
73
  console.error('请求失败!')
73
74
  return Promise.reject(res)
74
75
  },
75
- error => {
76
+ async error => {
77
+ const originalRequest = error.config || {}
78
+
79
+ if (error.response?.status === 401 && shouldUseV2Refresh() && !shouldSkipV2Refresh(originalRequest)) {
80
+ try {
81
+ originalRequest._retry = true
82
+ const newToken = await refreshV2AccessToken()
83
+
84
+ originalRequest.headers = {
85
+ ...originalRequest.headers,
86
+ Authorization: `Bearer ${newToken}`
87
+ }
88
+
89
+ return http(originalRequest)
90
+ } catch (refreshError) {
91
+ emitter.emit('error', refreshError)
92
+ return Promise.reject(refreshError)
93
+ }
94
+ }
95
+
76
96
  if (error.response && error.response.status) {
77
97
  switch (error.response.status) {
78
98
  case 401: