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 +33 -1
- package/package.json +1 -1
- package/utils/http-auth.js +192 -0
- package/utils/http-factory.js +22 -2
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
|
@@ -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
|
+
}
|
package/utils/http-factory.js
CHANGED
|
@@ -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:
|