nayota-show-sdk 1.3.95 → 1.3.97
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/api/devices.js +8 -0
- 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/utils/iot-module-specs.js +34 -0
- package/utils/iot-module-specs.test.js +51 -0
package/api/devices.js
CHANGED
|
@@ -399,6 +399,10 @@ export function getPropHistoryData(data) {
|
|
|
399
399
|
* }
|
|
400
400
|
*/
|
|
401
401
|
export function updateDepart(data) {
|
|
402
|
+
if (shouldUseIot('updateDepart')) {
|
|
403
|
+
return executeIotModuleAction(MODULE_NAME, 'updateDepart', data)
|
|
404
|
+
}
|
|
405
|
+
|
|
402
406
|
return requestShow({
|
|
403
407
|
url: '/devices/updateDepart',
|
|
404
408
|
method: 'post',
|
|
@@ -407,6 +411,10 @@ export function updateDepart(data) {
|
|
|
407
411
|
}
|
|
408
412
|
|
|
409
413
|
export function updateProp(data) {
|
|
414
|
+
if (shouldUseIot('updateProp')) {
|
|
415
|
+
return executeIotModuleAction(MODULE_NAME, 'updateProp', data)
|
|
416
|
+
}
|
|
417
|
+
|
|
410
418
|
return requestShow({
|
|
411
419
|
url: '/devices/updateProp',
|
|
412
420
|
method: 'post',
|
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:
|
|
@@ -1195,6 +1195,40 @@ export const iotModuleSpecs = {
|
|
|
1195
1195
|
message: data?.message ?? payload?.message
|
|
1196
1196
|
})
|
|
1197
1197
|
}
|
|
1198
|
+
},
|
|
1199
|
+
updateDepart: {
|
|
1200
|
+
toRequest(data = {}) {
|
|
1201
|
+
return {
|
|
1202
|
+
url: '/devices/updateDepart',
|
|
1203
|
+
method: 'post',
|
|
1204
|
+
data
|
|
1205
|
+
}
|
|
1206
|
+
},
|
|
1207
|
+
fromResponse(response) {
|
|
1208
|
+
const payload = response && response.data && response.status ? response.data : response
|
|
1209
|
+
const data = payload?.data !== undefined ? payload.data : payload
|
|
1210
|
+
return removeUndefinedFields({
|
|
1211
|
+
code: Number(data?.code ?? payload?.code ?? 0),
|
|
1212
|
+
message: data?.message ?? payload?.message ?? '更新成功'
|
|
1213
|
+
})
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
updateProp: {
|
|
1217
|
+
toRequest(data = {}) {
|
|
1218
|
+
return {
|
|
1219
|
+
url: '/devices/updateProp',
|
|
1220
|
+
method: 'post',
|
|
1221
|
+
data
|
|
1222
|
+
}
|
|
1223
|
+
},
|
|
1224
|
+
fromResponse(response) {
|
|
1225
|
+
const payload = response && response.data && response.status ? response.data : response
|
|
1226
|
+
const data = payload?.data !== undefined ? payload.data : payload
|
|
1227
|
+
return removeUndefinedFields({
|
|
1228
|
+
code: Number(data?.code ?? payload?.code ?? 0),
|
|
1229
|
+
message: data?.message ?? payload?.message ?? '更新成功'
|
|
1230
|
+
})
|
|
1231
|
+
}
|
|
1198
1232
|
}
|
|
1199
1233
|
}
|
|
1200
1234
|
},
|
|
@@ -735,3 +735,54 @@ describe('iotModuleSpecs devices getPropHistoryData legacy compatibility', () =>
|
|
|
735
735
|
})
|
|
736
736
|
})
|
|
737
737
|
})
|
|
738
|
+
|
|
739
|
+
describe('iotModuleSpecs devices legacy mutation compatibility', () => {
|
|
740
|
+
const devices = iotModuleSpecs.devices.operations
|
|
741
|
+
|
|
742
|
+
test('posts updateDepart to the v2 legacy-compatible endpoint', () => {
|
|
743
|
+
const payload = {
|
|
744
|
+
deviceIds: ['twin-1', 'twin-2'],
|
|
745
|
+
depart: 'hierarchy-1'
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const request = devices.updateDepart.toRequest(payload)
|
|
749
|
+
|
|
750
|
+
expect(request).toEqual({
|
|
751
|
+
url: '/devices/updateDepart',
|
|
752
|
+
method: 'post',
|
|
753
|
+
data: payload
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
expect(devices.updateDepart.fromResponse({ code: 0 })).toEqual({
|
|
757
|
+
code: 0,
|
|
758
|
+
message: '更新成功'
|
|
759
|
+
})
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
test('posts updateProp to the v2 legacy-compatible endpoint', () => {
|
|
763
|
+
const payload = {
|
|
764
|
+
deviceId: 'twin-1',
|
|
765
|
+
iotDeviceId: 'device-1',
|
|
766
|
+
props: [
|
|
767
|
+
{
|
|
768
|
+
type: 'Operate',
|
|
769
|
+
address: 'powerSwitch',
|
|
770
|
+
name: '开关'
|
|
771
|
+
}
|
|
772
|
+
]
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const request = devices.updateProp.toRequest(payload)
|
|
776
|
+
|
|
777
|
+
expect(request).toEqual({
|
|
778
|
+
url: '/devices/updateProp',
|
|
779
|
+
method: 'post',
|
|
780
|
+
data: payload
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
expect(devices.updateProp.fromResponse({ code: 0, message: '更新成功' })).toEqual({
|
|
784
|
+
code: 0,
|
|
785
|
+
message: '更新成功'
|
|
786
|
+
})
|
|
787
|
+
})
|
|
788
|
+
})
|