lgsso-sdk 1.2.7 → 1.2.9

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/src/sso.js CHANGED
@@ -1,6 +1,66 @@
1
1
  import { request } from './request';
2
2
  import { isBrowser, getQueryParam, removeQueryParam, getCurrentUrlWithParams, mergeConfigs } from './utils';
3
3
 
4
+ // ===== Cookie操作工具函数 =====
5
+ /**
6
+ * 获取指定名称的Cookie值
7
+ * @param {string} name Cookie名称
8
+ * @returns {string|null} Cookie值
9
+ */
10
+ function getCookie(name) {
11
+ if (!isBrowser()) return null;
12
+ const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
13
+ return match ? decodeURIComponent(match[2]) : null;
14
+ }
15
+
16
+ /**
17
+ * 设置Cookie
18
+ * @param {string} name Cookie名称
19
+ * @param {string} value Cookie值
20
+ * @param {Object} options 配置(过期时间、路径等)
21
+ */
22
+ function setCookie(name, value, options = {}) {
23
+ if (!isBrowser()) return;
24
+ let cookieStr = `${name}=${encodeURIComponent(value)}`;
25
+ cookieStr += `; path=${options.path || '/'}`; // 默认根路径
26
+ if (options.expires) {
27
+ const expires = typeof options.expires === 'number'
28
+ ? new Date(Date.now() + options.expires * 86400000)
29
+ : options.expires;
30
+ cookieStr += `; expires=${expires.toUTCString()}`;
31
+ }
32
+ document.cookie = cookieStr;
33
+ }
34
+
35
+ /**
36
+ * 删除指定名称的Cookie
37
+ * @param {string} name Cookie名称
38
+ */
39
+ function removeCookie(name) {
40
+ if (!isBrowser()) return;
41
+ setCookie(name, '', { expires: -1 });
42
+ }
43
+
44
+ /**
45
+ * 给重定向地址拼接platType参数
46
+ * @param {string} redirectUrl 原始重定向地址
47
+ * @param {string} platType platType值
48
+ * @param {string} platTypeKey 参数名(默认platType)
49
+ * @returns {string} 拼接后的重定向地址
50
+ */
51
+ function addPlatTypeToRedirectUrl(redirectUrl, platType, platTypeKey = 'platType') {
52
+ if (!redirectUrl || !platType) return redirectUrl;
53
+ try {
54
+ const url = new URL(redirectUrl);
55
+ url.searchParams.set(platTypeKey, platType);
56
+ return url.toString();
57
+ } catch (e) {
58
+ // 兼容非标准URL的情况(比如相对路径)
59
+ const separator = redirectUrl.includes('?') ? '&' : '?';
60
+ return `${redirectUrl}${separator}${platTypeKey}=${platType}`;
61
+ }
62
+ }
63
+
4
64
  // 默认配置
5
65
  const DEFAULT_CONFIG = {
6
66
  accessCodeKey: 'accessCode',
@@ -11,13 +71,14 @@ const DEFAULT_CONFIG = {
11
71
  refreshCodeApi: '',
12
72
  logoutApi: '',
13
73
  logOutUrl: '',
14
- storage: localStorage, // 可替换为sessionStorage
74
+ storage: localStorage,
15
75
  oldPwdKey: 'oldPwd',
16
76
  newPwdKey: 'newPwd',
17
77
  changePasswordApi: '',
18
78
  sendCaptchaCodeApi: '',
19
79
  typeKey: 'type',
20
- codeKey: 'code'
80
+ codeKey: 'code',
81
+ platTypeKey: 'platType' // platType参数名配置
21
82
  };
22
83
 
23
84
  let config = null;
@@ -40,46 +101,35 @@ function validateConfig(options) {
40
101
  }
41
102
 
42
103
  /**
43
- * 判断是否是iOS移动端(精准适配iPad Air/所有iPad机型 + 全版本iPadOS)
104
+ * 判断是否是iOS移动端
44
105
  * @returns {boolean} 是否为iOS环境
45
106
  */
46
107
  function isIOS() {
47
- // 非浏览器环境直接返回false(如需保留请取消注释)
48
- // if (!isBrowser()) return false;
108
+ if (!isBrowser()) return false;
49
109
 
50
110
  const userAgent = navigator.userAgent.toLowerCase();
51
- const platform = navigator.platform.toLowerCase(); // 统一转小写,避免大小写兼容问题
111
+ const platform = navigator.platform.toLowerCase();
52
112
  const maxTouchPoints = navigator.maxTouchPoints || 0;
53
- const screenRatio = screen.width / screen.height; // 屏幕宽高比(iPad核心特征:4:3左右)
113
+ const screenRatio = screen.width / screen.height;
54
114
 
55
- // ========== 核心检测规则 ==========
56
- // 1. 基础规则:iPhone/iPod 直接命中(无兼容问题)
57
115
  const isIphoneIpod = /iphone|ipod/.test(userAgent) && !window.MSStream;
58
-
59
- // 2. iPad 专属检测(覆盖所有iPad机型,含iPad Air)
60
116
  const isIpad = (
61
- // 场景1:老版本iPadOS/原生标识(platform含ipad)
62
117
  platform.includes('ipad') ||
63
- // 场景2:iPadOS 13+ 伪装Mac(UA含macintosh + 触摸+ 非Mac平台)
64
118
  (
65
- userAgent.includes('macintosh') &&
66
- maxTouchPoints > 0 && // iPad Air 触摸点≥5,Mac几乎为0
67
- !platform.includes('mac') && // 排除真Mac
119
+ userAgent.includes('macintosh') &&
120
+ maxTouchPoints > 0 &&
121
+ !platform.includes('mac') &&
68
122
  !window.MSStream
69
123
  ) ||
70
- // 场景3:新版本iPadOS(UA直接含ipados,如iPad Air搭载的iPadOS 15+)
71
124
  userAgent.includes('ipados') ||
72
- // 场景4:极端场景(第三方浏览器如Chrome for iPad Air)
73
125
  (
74
- maxTouchPoints >= 5 && // iPad Air 固定支持5点触摸
75
- !platform.includes('android') && // 排除安卓平板
76
- (screenRatio > 0.7 && screenRatio < 1.4) // iPad宽高比≈0.75(4:3),Mac多为1.78(16:9)
126
+ maxTouchPoints >= 5 &&
127
+ !platform.includes('android') &&
128
+ (screenRatio > 0.7 && screenRatio < 1.4)
77
129
  )
78
130
  );
79
131
 
80
- // ========== 最终判定 ==========
81
- const result = isIphoneIpod || isIpad;
82
- return result;
132
+ return isIphoneIpod || isIpad;
83
133
  }
84
134
 
85
135
  /**
@@ -99,9 +149,17 @@ export function createSSO() {
99
149
  config = mergeConfigs(DEFAULT_CONFIG, options);
100
150
  validateConfig(config);
101
151
 
152
+ // 初始化时检测URL中的platType并存入Cookie
153
+ if (isBrowser()) {
154
+ const platTypeFromUrl = getQueryParam(config.platTypeKey);
155
+ if (platTypeFromUrl) {
156
+ setCookie(config.platTypeKey, platTypeFromUrl);
157
+ // 可选:移除URL中的platType(避免重复显示)
158
+ }
159
+ }
160
+
102
161
  const accessCode = getQueryParam(config.accessCodeKey);
103
162
 
104
- // 验证必要的API配置
105
163
  if (!config.tokenApi) {
106
164
  return { code: -100, msg: '缺少tokenApi配置', success: false };
107
165
  }
@@ -120,14 +178,25 @@ export function createSSO() {
120
178
 
121
179
  if (result.code === 0 && result.data) {
122
180
  config.storage.setItem(config.tokenKey, result.data);
123
- removeQueryParam(config.accessCodeKey);
181
+ removeQueryParam([config.accessCodeKey,config.platTypeKey]);
124
182
  }
125
183
  return result;
126
184
  }
127
185
  // 如果没有token,跳转到登录页
128
186
  else if (!this.getToken()) {
129
187
  if (isBrowser() && config.logOutUrl) {
130
- window.location.href = `${config.logOutUrl}?redirect_uri=${encodeURIComponent(getCurrentUrlWithParams())}`;
188
+ // ===== 核心修正:platType拼到redirect_uri里 =====
189
+ const platTypeFromCookie = getCookie(config.platTypeKey);
190
+ // 1. 获取原始重定向地址
191
+ let redirectUri = getCurrentUrlWithParams();
192
+ // 2. 给重定向地址加platType参数
193
+ if (platTypeFromCookie) {
194
+ redirectUri = addPlatTypeToRedirectUrl(redirectUri, platTypeFromCookie, config.platTypeKey);
195
+ }
196
+ // 3. 构建登录页URL(仅带redirect_uri参数)
197
+ let loginUrl = new URL(config.logOutUrl);
198
+ loginUrl.searchParams.set('redirect_uri', encodeURIComponent(redirectUri));
199
+ window.location.href = loginUrl.toString();
131
200
  }
132
201
  }
133
202
 
@@ -159,33 +228,21 @@ export function createSSO() {
159
228
  * @returns {Promise<Object>} 接口返回结果
160
229
  */
161
230
  async logout() {
162
- // 1. 前置校验:初始化状态
163
231
  if (!config) {
164
232
  return { code: -101, msg: '请先调用init方法初始化', success: false };
165
233
  }
166
234
 
167
- // 2. 工具函数:统一构建登录跳转URL(确保完整编码当前URL的所有参数/hash)
168
- const buildLoginUrl = () => {
169
- if (!config.logOutUrl || !isBrowser()) return '';
170
- // 关键:获取当前完整URL(含query、hash)并完整编码,避免参数丢失
171
- const currentFullUrl = window.location.href;
172
- const encodedRedirectUri = encodeURIComponent(currentFullUrl);
173
- return `${config.logOutUrl}?redirect_uri=${encodedRedirectUri}`;
174
- };
175
-
176
- // 3. 校验logoutApi配置(仅接口调用时需要,跳转登录页不受此影响)
177
235
  if (!config.logoutApi) {
178
- // 无logoutApi时仍清除token并跳转登录页,保证基础退出逻辑
179
- this.removeToken();
180
- const loginUrl = buildLoginUrl();
181
- if (loginUrl) window.location.href = loginUrl;
182
236
  return { code: -102, msg: '未配置logoutApi', success: false };
183
237
  }
184
238
 
239
+ // 获取并删除Cookie中的platType
240
+ const platType = getCookie(config.platTypeKey);
241
+ removeCookie(config.platTypeKey);
242
+
185
243
  const token = this.getToken();
186
244
  if (token) {
187
245
  try {
188
- // 调用退出接口
189
246
  const result = await request(
190
247
  config.logoutApi,
191
248
  {
@@ -194,37 +251,48 @@ export function createSSO() {
194
251
  timeout: config.timeout
195
252
  }
196
253
  );
197
- // 无论接口返回结果如何,都清除token
198
254
  this.removeToken();
199
255
 
200
- // 跳转登录页(携带完整的当前URL)
201
- const loginUrl = buildLoginUrl();
202
- if (loginUrl) window.location.href = loginUrl;
256
+ if (isBrowser() && config.logOutUrl) {
257
+ // ===== 核心修正:platType拼到redirect_uri里 =====
258
+ // 1. 获取原始重定向地址
259
+ let redirectUri = getCurrentUrlWithParams();
260
+ // 2. 给重定向地址加platType参数
261
+ if (platType) {
262
+ redirectUri = addPlatTypeToRedirectUrl(redirectUri, platType, config.platTypeKey);
263
+ }
264
+ // 3. 构建登录页URL
265
+ let logoutUrl = new URL(config.logOutUrl);
266
+ logoutUrl.searchParams.set('redirect_uri', encodeURIComponent(redirectUri));
267
+ window.location.href = logoutUrl.toString();
268
+ }
203
269
  return result;
204
270
  } catch (error) {
205
- // 接口调用失败,仍清除token并跳转登录页
206
- this.removeToken();
207
- const loginUrl = buildLoginUrl();
208
- if (loginUrl) window.location.href = loginUrl;
209
271
  return { code: -103, msg: `退出失败: ${error.message}`, success: false };
210
272
  }
211
273
  } else {
212
- // 无token时直接清除(兜底)并跳转登录页
213
274
  this.removeToken();
214
- const loginUrl = buildLoginUrl();
215
- if (loginUrl) window.location.href = loginUrl;
275
+ if (isBrowser() && config.logOutUrl) {
276
+ // ===== 核心修正:platType拼到redirect_uri里 =====
277
+ let redirectUri = getCurrentUrlWithParams();
278
+ if (platType) {
279
+ redirectUri = addPlatTypeToRedirectUrl(redirectUri, platType, config.platTypeKey);
280
+ }
281
+ let logoutUrl = new URL(config.logOutUrl);
282
+ logoutUrl.searchParams.set('redirect_uri', encodeURIComponent(redirectUri));
283
+ window.location.href = logoutUrl.toString();
284
+ }
216
285
  return { code: 0, msg: '已成功清除token', success: true };
217
286
  }
218
287
  },
219
288
 
220
289
  /**
221
290
  * 用token换取新accessCode并跳转到指定URL
222
- * @param {string} redirectUrl - 目标跳转地址(支持带query/hash参数)
291
+ * @param {string} redirectUrl - 目标跳转地址
223
292
  * @param {string} target - 当前页面:_self、新页面打开:_blank,默认当前页_self
224
293
  * @returns {Promise<Object>} 接口返回结果
225
294
  */
226
295
  async toUrl(redirectUrl, target = '_self') {
227
- // 1. 前置校验:初始化状态、参数合法性
228
296
  if (!config) {
229
297
  return { code: -101, msg: '请先调用init方法初始化', success: false };
230
298
  }
@@ -233,45 +301,49 @@ export function createSSO() {
233
301
  return { code: -104, msg: '请提供跳转地址', success: false };
234
302
  }
235
303
 
236
- // ========== 新增:获取当前页面的platType参数 ==========
237
- const currentPlatType = isBrowser() ? getQueryParam('platType') : '';
238
-
239
- // ========== 保留:platType=screen强制_self逻辑 ==========
240
- const isPlatTypeScreen = currentPlatType === 'screen';
241
-
242
- // 2. 处理target参数:platType=screen > iOS > 传入的target
243
- const finalTarget = isPlatTypeScreen
244
- ? '_self' // platType=screen时强制_self
245
- : (isIOS() ? '_self' : target); // 否则沿用原iOS判断逻辑
246
-
247
- if (!['_self', '_blank'].includes(finalTarget)) {
248
- return { code: -108, msg: 'target参数必须是"_self"或"_blank"', success: false };
304
+ // iOS强制_self
305
+ if (isIOS()) {
306
+ target = '_self';
249
307
  }
250
308
 
251
- // 3. 工具函数:统一构建登录跳转URL(避免重复代码,确保参数完整编码)
252
- const buildLoginUrl = (redirectUrl) => {
253
- if (!config.logOutUrl) return '';
254
- // 关键:encodeURIComponent 会完整编码redirectUrl的所有部分(query/hash),避免参数丢失
255
- const encodedRedirectUri = encodeURIComponent(redirectUrl);
256
- return `${config.logOutUrl}?redirect_uri=${encodedRedirectUri}`;
257
- };
309
+ // platType=screen时强制_self
310
+ const platType = getCookie(config.platTypeKey);
311
+ if (platType === 'screen') {
312
+ target = '_self';
313
+ }
314
+
315
+ // 验证target参数
316
+ if (!['_self', '_blank'].includes(target)) {
317
+ return { code: -108, msg: 'target参数必须是"_self"或"_blank"', success: false };
318
+ }
258
319
 
259
- // 4. 校验refreshCodeApi配置
260
320
  if (!config.refreshCodeApi) {
261
321
  return { code: -105, msg: '未配置refreshCodeApi', success: false };
262
322
  }
263
323
 
264
- // 5. 获取token,无token则直接跳登录页
265
324
  const token = this.getToken();
266
325
  if (!token) {
267
326
  if (isBrowser() && config.logOutUrl) {
268
- const loginUrl = buildLoginUrl(redirectUrl);
269
- finalTarget === '_blank' ? window.open(loginUrl, '_blank') : window.location.href = loginUrl;
327
+ // ===== 核心修正:platType拼到redirect_uri里 =====
328
+ // 1. 给目标跳转地址加platType
329
+ let newRedirectUrl = redirectUrl;
330
+ if (platType) {
331
+ newRedirectUrl = addPlatTypeToRedirectUrl(newRedirectUrl, platType, config.platTypeKey);
332
+ }
333
+ // 2. 构建登录页URL
334
+ let loginUrl = new URL(config.logOutUrl);
335
+ loginUrl.searchParams.set('redirect_uri', encodeURIComponent(newRedirectUrl));
336
+ const loginUrlStr = loginUrl.toString();
337
+
338
+ if (target === '_blank') {
339
+ window.open(loginUrlStr, '_blank');
340
+ } else {
341
+ window.location.href = loginUrlStr;
342
+ }
270
343
  }
271
344
  return { code: -106, msg: '未找到有效token', success: false };
272
345
  }
273
346
 
274
- // 6. 有token则尝试换取accessCode并跳转
275
347
  try {
276
348
  const result = await request(
277
349
  config.refreshCodeApi,
@@ -283,31 +355,48 @@ export function createSSO() {
283
355
  );
284
356
 
285
357
  if (result.code === 0 && result.data && isBrowser()) {
286
- // ========== 核心优化:拼接platType到目标URL ==========
358
+ // toUrl跳转目标地址时,仅携带accessCode,不携带platType
287
359
  const url = new URL(redirectUrl);
288
- // 1. 如果当前页面有platType,拼接到目标URL(覆盖原有platType)
289
- if (currentPlatType) {
290
- url.searchParams.set('platType', currentPlatType);
291
- }
292
- // 2. 拼接accessCode(保留原有逻辑)
293
360
  url.searchParams.set(config.accessCodeKey, result.data);
294
361
  const targetUrl = url.toString();
295
362
 
296
- finalTarget === '_blank' ? window.open(targetUrl, '_blank') : window.location.href = targetUrl;
363
+ if (target === '_blank') {
364
+ window.open(targetUrl, '_blank');
365
+ } else {
366
+ window.location.href = targetUrl;
367
+ }
297
368
  } else {
298
- // 接口返回失败,跳登录页(复用统一的登录URL构建逻辑)
299
- if (isBrowser() && config.logOutUrl) {
300
- const loginUrl = buildLoginUrl(redirectUrl);
301
- finalTarget === '_blank' ? window.open(loginUrl, '_blank') : window.location.href = loginUrl;
369
+ // ===== 核心修正:platType拼到redirect_uri里 =====
370
+ let newRedirectUrl = redirectUrl;
371
+ if (platType) {
372
+ newRedirectUrl = addPlatTypeToRedirectUrl(newRedirectUrl, platType, config.platTypeKey);
373
+ }
374
+ let loginUrl = new URL(config.logOutUrl);
375
+ loginUrl.searchParams.set('redirect_uri', encodeURIComponent(newRedirectUrl));
376
+ const loginUrlStr = loginUrl.toString();
377
+
378
+ if (target === '_blank') {
379
+ window.open(loginUrlStr, '_blank');
380
+ } else {
381
+ window.location.href = loginUrlStr;
302
382
  }
303
383
  }
304
384
 
305
385
  return result;
306
386
  } catch (error) {
307
- // 接口异常,跳登录页(复用统一的登录URL构建逻辑)
308
- if (isBrowser() && config.logOutUrl) {
309
- const loginUrl = buildLoginUrl(redirectUrl);
310
- finalTarget === '_blank' ? window.open(loginUrl, '_blank') : window.location.href = loginUrl;
387
+ // ===== 核心修正:platType拼到redirect_uri里 =====
388
+ let newRedirectUrl = redirectUrl;
389
+ if (platType) {
390
+ newRedirectUrl = addPlatTypeToRedirectUrl(newRedirectUrl, platType, config.platTypeKey);
391
+ }
392
+ let loginUrl = new URL(config.logOutUrl);
393
+ loginUrl.searchParams.set('redirect_uri', encodeURIComponent(newRedirectUrl));
394
+ const loginUrlStr = loginUrl.toString();
395
+
396
+ if (target === '_blank') {
397
+ window.open(loginUrlStr, '_blank');
398
+ } else {
399
+ window.location.href = loginUrlStr;
311
400
  }
312
401
  return { code: -107, msg: `跳转失败: ${error.message}`, success: false };
313
402
  }
@@ -346,7 +435,7 @@ export function createSSO() {
346
435
  * @returns {Object|null} 当前配置
347
436
  */
348
437
  getConfig() {
349
- return { ...config }; // 返回配置的副本,防止外部修改
438
+ return { ...config };
350
439
  },
351
440
 
352
441
  async changePassword(fromData = {}) {
@@ -375,7 +464,6 @@ export function createSSO() {
375
464
 
376
465
  async getPhoneCode() {
377
466
  const token = this.getToken();
378
- // 修复笔误:sendCaptchaCode → sendCaptchaCodeApi
379
467
  return request(
380
468
  config.sendCaptchaCodeApi,
381
469
  {
package/src/utils.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  * @Author: Robin LEI
3
3
  * @Date: 2025-08-21 15:09:15
4
- * @LastEditTime: 2025-08-21 15:22:11
5
- * @FilePath: \lg-wms-admind:\业务代码\中联钢信\五服一管\lg-ssosdk\src\utils.js
4
+ * @LastEditTime: 2026-01-23 11:28:26
5
+ * @FilePath: \lims-frontd:\业务代码\中联钢信\五服一管\lg-ssosdk\src\utils.js
6
6
  */
7
7
  /**
8
8
  * 检查是否在浏览器环境
@@ -38,38 +38,58 @@ export function getQueryParam(name, url) {
38
38
  }
39
39
 
40
40
  /**
41
- * 移除URL中的指定参数
42
- * @param {string} name - 参数名
41
+ * 移除URL中的指定参数(支持单个或多个)
42
+ * @param {string|string[]} names - 要删除的参数名(单个字符串或字符串数组)
43
43
  */
44
- export function removeQueryParam(name) {
44
+ export function removeQueryParam(names) {
45
45
  if (!isBrowser()) return;
46
46
 
47
+ // 统一转为数组,兼容单个参数的调用方式
48
+ const paramNames = Array.isArray(names) ? names : [names];
49
+ if (paramNames.length === 0) return; // 无参数需删除时直接返回
50
+
47
51
  const url = new URL(window.location.href);
48
- let params, newUrl;
52
+ let newUrl;
49
53
 
50
54
  // 处理hash模式(参数在#之后)
51
55
  if (url.hash.includes('?')) {
52
56
  const [hashPath, hashQuery] = url.hash.split('?');
53
- params = new URLSearchParams(hashQuery);
57
+ const params = new URLSearchParams(hashQuery);
58
+
59
+ // 批量删除参数
60
+ paramNames.forEach(name => {
61
+ if (params.has(name)) {
62
+ params.delete(name);
63
+ }
64
+ });
54
65
 
55
- if (params.has(name)) {
56
- params.delete(name);
57
- const newHash = params.toString() ? `${hashPath}?${params.toString()}` : hashPath;
66
+ // 重构hash部分
67
+ const newHash = params.toString() ? `${hashPath}?${params.toString()}` : hashPath;
68
+ if (newHash !== url.hash) { // 只有hash变化时才更新URL
58
69
  url.hash = newHash;
59
70
  newUrl = url.toString();
60
71
  }
61
72
  }
62
73
  // 处理history模式(参数在?之后)
63
74
  else {
64
- params = new URLSearchParams(url.search);
65
- if (params.has(name)) {
66
- params.delete(name);
75
+ const params = new URLSearchParams(url.search);
76
+ let hasChanged = false;
77
+
78
+ // 批量删除参数
79
+ paramNames.forEach(name => {
80
+ if (params.has(name)) {
81
+ params.delete(name);
82
+ hasChanged = true;
83
+ }
84
+ });
85
+
86
+ // 只有参数变化时才更新URL
87
+ if (hasChanged) {
67
88
  url.search = params.toString();
68
89
  newUrl = url.toString();
69
90
  }
70
91
  }
71
-
72
- // 更新URL
92
+ // 更新URL(仅当URL有变化时)
73
93
  if (newUrl && newUrl !== window.location.href) {
74
94
  window.location.replace(newUrl);
75
95
  }
@@ -92,6 +112,7 @@ export function getCurrentUrlWithParams() {
92
112
  */
93
113
  export function mergeConfigs(defaults, options) {
94
114
  if (!options) return { ...defaults };
115
+
95
116
  const merged = { ...defaults };
96
117
  for (const key in options) {
97
118
  if (options.hasOwnProperty(key)) {