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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nayota-show-sdk",
3
- "version": "1.3.95",
3
+ "version": "1.3.97",
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:
@@ -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
+ })