shaogen1995 3 hafta önce
ebeveyn
işleme
89b8f5b6bc

+ 2 - 0
.gitignore

@@ -17,6 +17,8 @@
 .env.development.local
 .env.test.local
 .env.production.local
+.env
+key.txt
 
 npm-debug.log*
 yarn-debug.log*

+ 3 - 0
package.json

@@ -7,6 +7,7 @@
     "@testing-library/jest-dom": "^5.16.5",
     "@testing-library/react": "^13.4.0",
     "@testing-library/user-event": "^13.5.0",
+    "@types/crypto-js": "^4.2.2",
     "@types/jest": "^27.5.2",
     "@types/node": "^16.18.3",
     "@types/react": "^18.0.24",
@@ -17,9 +18,11 @@
     "axios": "^1.1.3",
     "braft-editor": "^2.3.9",
     "braft-utils": "^3.0.12",
+    "crypto-js": "^4.2.0",
     "dayjs": "^1.11.10",
     "hammerjs": "^2.0.8",
     "js-base64": "^3.7.3",
+    "jsencrypt": "^3.5.4",
     "react": "^18.2.0",
     "react-countup": "^6.5.3",
     "react-dom": "^18.2.0",

+ 5 - 4
public/index.html

@@ -14,11 +14,12 @@
 
     <title>全国总工会虚拟展会</title>
 
-    <!-- 甲方接口前缀 -->
     <script>
-      // gjdhvr-test
-      // MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOkm4dGsnfw6uOfwUkIkwagf13rqaXkOI3xmmgFwHVvJDZAHkCpF3yk1a9Zy19afoQGas3+rYX4lGVYO0maCxcZ4HyqzLLbjnEq7GJKsBUHC4KVeBqYavhpunIMiOUOCmuEROFd/4PPe6eg/AtH2Ipb3RFmBBAoVWCQEFcbYJjVQIDAQAB
-      const httpBaseUrl = 'https://jnqg.test.zhzg.vip/API-ENTERPRISE-OPENAPI'
+      // 甲方公钥
+      const publicKeyTemp =
+        'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOkm4dGsnfw6uOfwUkIkwagf13rqaXkOI3xmmgFwHVvJDZAHkCpF3yk1a9Zy19afoQGas3+rYX4lGVYO0maCxcZ4HyqzLLbjnEq7GJKsBUHC4KVeBqYavhpunIMiOUOCmuEROFd/4PPe6eg/AtH2Ipb3RFmBBAoVWCQEFcbYJjVQIDAQAB'
+      // 甲方接口前缀
+      const httpBaseUrl = 'https://jnqg.test.zhzg.vip/API-ENTERPRISE-OPENAPI/'
     </script>
   </head>
   <body>

+ 2 - 0
src/components/RoutePage.tsx

@@ -4,12 +4,14 @@ import NotFound from './NotFound'
 
 const A1home = React.lazy(() => import('../pages/A1home'))
 const A2scene = React.lazy(() => import('../pages/A2scene'))
+const ErrPage = React.lazy(() => import('../pages/ErrPage'))
 
 function RoutePage() {
   return (
     <Switch>
       <Route path='/scene/:code' component={A2scene} exact />
       <Route path='/' component={A1home} exact />
+      <Route path='/err' component={ErrPage} exact />
       {/* 找不到页面 */}
       <Route path='*' component={NotFound} />
     </Switch>

+ 10 - 4
src/pages/A1home/A1svgBox/index.tsx

@@ -106,7 +106,9 @@ function A1svgBox({ opc = 1, id = '', closeFu }: Props) {
   }, [])
 
   // 当前展馆选中
-  const { svgAcGlobal, userPhone } = useSelector((state: RootState) => state.A0Layout)
+  const { svgAcGlobal, ttsxsysid, myPhone } = useSelector(
+    (state: RootState) => state.A0Layout
+  )
 
   useEffect(() => {
     if (svgAcGlobal) {
@@ -135,7 +137,7 @@ function A1svgBox({ opc = 1, id = '', closeFu }: Props) {
           }
           dom.onclick = () => {
             store.dispatch({ type: 'layout/svgAcGlobal', payload: obj.code })
-            history.replace(`/scene/${obj.code}?n=${userPhone}`)
+            history.replace(`/scene/${obj.code}?k=${ttsxsysid}&n=${myPhone}`)
             if (closeFu) closeFu()
           }
 
@@ -145,14 +147,18 @@ function A1svgBox({ opc = 1, id = '', closeFu }: Props) {
         }
       })
     }
-  }, [closeFu, svgAcGlobal, userPhone])
+  }, [closeFu, myPhone, svgAcGlobal, ttsxsysid])
 
   useEffect(() => {
     pathAllFu()
   }, [pathAllFu])
 
   return (
-    <div className={styles.A1svgBox} style={{ opacity: opc }} id={id}>
+    <div
+      className={styles.A1svgBox}
+      style={{ opacity: opc, pointerEvents: opc ? 'auto' : 'none' }}
+      id={id}
+    >
       <A1svg initFu={dom => initFu(dom)} />
     </div>
   )

+ 8 - 0
src/pages/A1home/index.tsx

@@ -3,6 +3,7 @@ import styles from './index.module.scss'
 import { getCanFu, isMobileFu } from '@/utils/history'
 import A1svgBox from './A1svgBox'
 import ErrPage from '../ErrPage'
+// import { myGkey, myJiaMiFu, myJieMiFu, mySkey } from '@/utils/key'
 
 function A1home() {
   useEffect(() => {
@@ -22,6 +23,13 @@ function A1home() {
     getCanFu(obj => setIsNumFlag(obj))
   }, [])
 
+  // useEffect(() => {
+  //   const jimiData = myJiaMiFu('18702025090', myGkey)
+  //   console.log('我方公钥加密', jimiData)
+  //   const res = myJieMiFu(jimiData, mySkey)
+  //   console.log('我方私钥解密', res)
+  // }, [])
+
   return (
     <div className={styles.A1home}>
       {isNumFlag.flag ? <A1svgBox opc={isNumFlag.opc as 1} /> : <ErrPage />}

+ 5 - 0
src/pages/A2danList/index.module.scss

@@ -0,0 +1,5 @@
+.A2danList {
+  background-color: rgba(0, 0, 0, 0.6);
+  :global {
+  }
+}

+ 24 - 0
src/pages/A2danList/index.tsx

@@ -0,0 +1,24 @@
+import React, { useEffect, useState } from 'react'
+import styles from './index.module.scss'
+import { useDispatch, useSelector } from 'react-redux'
+import { getDanListApi } from '@/store/action/A1list'
+import { RootState } from '@/store'
+function A2danList() {
+  const dispatch = useDispatch()
+
+  const { userPhone } = useSelector((state: RootState) => state.A0Layout)
+
+  useEffect(() => {
+    dispatch(getDanListApi({ phone: userPhone }))
+  }, [dispatch, userPhone])
+
+  return (
+    <div className={styles.A2danList} id='A2openHome'>
+      <h1>A2danList</h1>
+    </div>
+  )
+}
+
+const MemoA2danList = React.memo(A2danList)
+
+export default MemoA2danList

+ 8 - 0
src/pages/A2scene/index.module.scss

@@ -36,5 +36,13 @@
       height: 100%;
       z-index: 20;
     }
+    // 彩蛋列表
+
+    .A2listOpen {
+      position: absolute;
+      top: 10px;
+      right: 10px;
+      z-index: 10;
+    }
   }
 }

+ 17 - 2
src/pages/A2scene/index.tsx

@@ -6,6 +6,7 @@ import ErrPage from '../ErrPage'
 import store, { RootState } from '@/store'
 import A1svgBox from '../A1home/A1svgBox'
 import { useSelector } from 'react-redux'
+import A2danList from '../A2danList'
 
 function A2scene() {
   // 参数是否正确
@@ -35,7 +36,10 @@ function A2scene() {
     setIsHome(true)
   }, [code])
 
-  const { userPhone } = useSelector((state: RootState) => state.A0Layout)
+  const { myPhone, ttsxsysid } = useSelector((state: RootState) => state.A0Layout)
+
+  // 彩蛋列表
+  const [listShow, setListShow] = useState(false)
 
   return (
     <div className={styles.A2scene}>
@@ -51,10 +55,21 @@ function A2scene() {
           <div className='A2Ren'>
             <div onClick={isHomeFu}>去首页</div>
             {/* 待完善 */}
-            <div onClick={() => history.replace(`/scene/SG-4cOtHp4T3Ax?n=${userPhone}`)}>
+            <div
+              onClick={() =>
+                history.replace(`/scene/SG-4cOtHp4T3Ax?k=${ttsxsysid}&n=${myPhone}`)
+              }
+            >
               跳场景
             </div>
           </div>
+
+          <div className='A2listOpen' onClick={() => setListShow(true)}>
+            打开彩蛋列表
+          </div>
+
+          {/* 彩蛋列表 */}
+          {listShow ? <A2danList /> : null}
         </div>
       ) : (
         <ErrPage />

+ 13 - 19
src/store/action/A1list.ts

@@ -3,28 +3,22 @@
  */
 
 import http from '@/utils/http'
+import { AppDispatch } from '..'
 
-// export const A1_APIgetList = (data: any): any => {
-//   return async (dispatch: AppDispatch) => {
-//     const res = await http.post('cms/applyExhibition/pageList', data)
-//     if (res.code === 0) {
-//       const arr: A1tableType[] = res.data.records
-//       arr.forEach(v => {
-//         v.cityStr = v.province + '-' + v.city
-//       })
+export const getDanListApi = (data: any): any => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post('openapi/taskVr/getEggStatusList', data)
 
-//       const obj = {
-//         list: arr,
-//         total: res.data.total
-//       }
-//       dispatch({ type: 'A1/getList', payload: obj })
-//     }
-//   }
-// }
+    console.log(123, res)
+
+    if (res.code === '200') {
+    }
+  }
+}
 
 /**
- * 获取全部访问量
+ * 检测手机号是否有效
  */
-export const A1_APIgetNumList = () => {
-  return http.get('visit/getList')
+export const checkNumAPI = (data: any) => {
+  return http.post('openapi/taskVr/checkPhone', data)
 }

+ 18 - 2
src/store/reducer/layout.ts

@@ -28,8 +28,14 @@ const initState = {
 
   // svg 的选中
   svgAcGlobal: '',
-  // 用户手机号
-  userPhone: ''
+  // 用户手机号 (我的公钥加密)
+  myPhone: '',
+  // 用户手机号 (甲方公钥加密)
+  userPhone: '',
+  // 用户手机号 (脱敏)
+  userPhoneNum: '',
+  // 甲方接口标识
+  ttsxsysid: ''
 }
 
 // 定义 action 类型
@@ -45,7 +51,10 @@ type LayoutActionType =
       }
     }
   | { type: 'layout/svgAcGlobal'; payload: string }
+  | { type: 'layout/myPhone'; payload: string }
   | { type: 'layout/userPhone'; payload: string }
+  | { type: 'layout/userPhoneNum'; payload: string }
+  | { type: 'layout/ttsxsysid'; payload: string }
 
 // 频道 reducer
 export default function layoutReducer(state = initState, action: LayoutActionType) {
@@ -68,8 +77,15 @@ export default function layoutReducer(state = initState, action: LayoutActionTyp
     case 'layout/svgAcGlobal':
       return { ...state, svgAcGlobal: action.payload }
     // 用户手机号
+    case 'layout/myPhone':
+      return { ...state, myPhone: action.payload }
     case 'layout/userPhone':
       return { ...state, userPhone: action.payload }
+    case 'layout/userPhoneNum':
+      return { ...state, userPhoneNum: action.payload }
+    // 甲方接口标识
+    case 'layout/ttsxsysid':
+      return { ...state, ttsxsysid: action.payload }
     default:
       return state
   }

+ 1 - 0
src/types/declaration.d.ts

@@ -8,5 +8,6 @@ declare module '*.svg'
 declare module 'js-export-excel'
 declare module 'braft-utils'
 declare module 'hammerjs'
+declare const publicKeyTemp: string
 
 declare const httpBaseUrl: string

+ 58 - 28
src/utils/history.ts

@@ -1,5 +1,8 @@
 import store from '@/store'
 import { createHashHistory } from 'history'
+import { checkNumAPI } from '@/store/action/A1list'
+import { myJiaMiFu, myJieMiFu, mySkey, publicKey } from './key'
+import { MessageFu } from './message'
 const history = createHashHistory()
 export default history
 
@@ -14,34 +17,61 @@ export const isMobileFu = () => {
   } else return false
 }
 
+const urlParameter = () => {
+  let data = window.location.href
+
+  if (data) {
+    const query = data.substring(data.indexOf('?') + 1)
+    const arr = query.split('&')
+    const params = {} as any
+    arr.forEach(v => {
+      const key = v.substring(0, v.indexOf('='))
+      const val = v.substring(v.indexOf('=') + 1)
+      params[key] = val
+    })
+    return params
+  } else return {}
+}
+
 // 检查参数是否正确
-export const getCanFu = (fu: (obj: any) => void) => {
-  const urlAll = window.location.href
-  const urlArr = urlAll.split('?n=')
-
-  if (urlArr && urlArr.length >= 2) {
-    const num = urlArr[1]
-    // 待完善
-    console.log(num)
-
-    // 仓库没有值得时候发接口
-    const userPhone = store.getState().A0Layout.userPhone
-
-    if (!userPhone) {
-      // 发送接口
-      console.log('发送接口核对手机号······························')
-
-      // 核验成功之后存到仓库
-      if (1 + 1 === 2) {
-        store.dispatch({ type: 'layout/userPhone', payload: num })
-        fu({ opc: 1, flag: true })
-      } else {
-        // 校验失败
-        fu({ opc: 0, flag: false })
-      }
-    } else if (userPhone === num) {
-      // 仓库有值并且和地址栏的值相同
-      fu({ opc: 1, flag: true })
-    }
+export const getCanFu = async (fu: (obj: any) => void) => {
+  const canObj = urlParameter()
+
+  if (canObj.k && canObj.n) {
+    const biaoShi = canObj.k
+
+    // 甲方接口标识存起来
+    store.dispatch({ type: 'layout/ttsxsysid', payload: biaoShi })
+
+    // 使用我的公钥加密的手机号
+    const num = canObj.n
+    store.dispatch({ type: 'layout/myPhone', payload: num })
+
+    // 转成正常手机号
+    const phone = myJieMiFu(num, mySkey) as string
+
+    // console.log(canObj, phone)
+
+    if (phone && phone.length >= 11) {
+      // 脱敏处理
+      const phoneRes = phone.slice(0, 3) + '****' + phone.slice(7)
+
+      // 存到仓库
+      store.dispatch({ type: 'layout/userPhoneNum', payload: phoneRes })
+
+      // 使用甲方的公钥加密正常手机号
+      const jiaNum = myJiaMiFu(phone, publicKey)
+
+      const res = await checkNumAPI({ phone: jiaNum })
+      if (res.code === '200') {
+        if (res.data) {
+          store.dispatch({ type: 'layout/userPhone', payload: jiaNum })
+          fu({ opc: 1, flag: true })
+        } else {
+          MessageFu.warning('手机号检验失败')
+          fu({ opc: 0, flag: false })
+        }
+      } else fu({ opc: 0, flag: false })
+    } else fu({ opc: 0, flag: false })
   } else fu({ opc: 0, flag: false })
 }

+ 27 - 8
src/utils/http.ts

@@ -1,8 +1,9 @@
 import axios from 'axios'
-import { getTokenInfo } from './storage'
 import store from '@/store'
 import { MessageFu } from './message'
 import { domShowFu } from './domShow'
+import dayjs from 'dayjs'
+import { mySkey, signData } from './key'
 
 const envFlag = process.env.NODE_ENV === 'development'
 
@@ -12,7 +13,7 @@ export const baseURL = envFlag ? httpBaseUrl : httpBaseUrl
 // 处理  类型“AxiosResponse<any, any>”上不存在属性“code”
 declare module 'axios' {
   interface AxiosResponse {
-    code: number
+    code: number | string
     timestamp: string
     // 这里追加你的参数
   }
@@ -34,8 +35,28 @@ http.interceptors.request.use(
 
     axajInd++
 
-    const { token } = getTokenInfo()
-    if (token) config.headers.token = token
+    const ttsxsysid = store.getState().A0Layout.ttsxsysid
+
+    const ttsxreqid = Date.now() + ''
+
+    const ttsxreqtime = dayjs().format('YYYYMMDDHHmmss')
+
+    // 对接系统的ID
+    config.headers.ttsxsysid = ttsxsysid
+    // 随机字符串,两分钟中内不能重复
+    config.headers.ttsxreqid = ttsxreqid
+    // 请求时间
+    config.headers.ttsxreqtime = ttsxreqtime
+    // 请求签名
+    const canJson = JSON.stringify(config.data || '')
+    const ttsxsignTemp = `${canJson}|${ttsxsysid}|${ttsxreqid}|${ttsxreqtime}`
+
+    const ttsxsign = signData(ttsxsignTemp, mySkey)
+
+    // console.log('---', ttsxsignTemp, ttsxsign)
+
+    config.headers.ttsxsign = ttsxsign
+
     return config
   },
   function (err) {
@@ -53,11 +74,9 @@ http.interceptors.response.use(
     if (axajInd === 0) {
       domShowFu('#AsyncSpinLoding', false)
     }
-    // 待完善 接口返回值规则
-    if (response.data.code === 5001 || response.data.code === 5002) {
-    } else if (response.data.code === 0) {
+    if (response.data.code === '200') {
       // MessageFu.success(response.data.msg);
-    } else if (response.data.code !== 3014) MessageFu.warning(response.data.msg)
+    } else MessageFu.warning(response.data.msg)
 
     return response.data
   },

+ 110 - 0
src/utils/key.ts

@@ -0,0 +1,110 @@
+import { JSEncrypt } from 'jsencrypt'
+import * as CryptoJS from 'crypto-js'
+
+// 我方公钥
+export const myGkey =
+  'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnIBg3mmubtZV6hjTybrC51HpGeD2SbQ++xwbs1x3Ve9q/kpXBowjn5uzwCgmbNv+SRLZFsI6llKiCo9sEeWkoH7TMKXMB4qR/1c+7OB+mZ7O6a3v7gMuVz8Qpe5xQohrEjnmAQgG9C3EYvH8CRnsn6QDiYZpK+Me1BRGKzKGzIzVpzs0Mt0C26Opbg7WdhhGso6Dkt+6Rb+P3awGirp0d24df/VFAUOtUqQ3YWtCynSWtpIJRG9D5Fu7PJEtJBJ3nupTRdUIq+WeIATcq0y8Fwcxv8Jfm4xJPndptti4fH9foC0FD5Zdcbb4rQSfxp1D89E+ca/t6P2dMxAEWn5UhQIDAQAB'
+
+// 我方私钥
+export const mySkey = process.env.REACT_APP_PRIVATE_KEY!.replace(/\\n/g, '\n')
+
+// 甲方公钥
+export const publicKey = publicKeyTemp
+
+// 使用公钥加密
+const encryptData = (data: string, publicKey: string): string | false => {
+  const encryptor = new JSEncrypt()
+  encryptor.setPublicKey(publicKey)
+  return encryptor.encrypt(data)
+}
+
+export const myJiaMiFu = (val: string, key: string) => {
+  const encryptedData = encryptData(val, key)
+  if (encryptedData) {
+    // console.log('加密后的数据:', encryptedData)
+    return encryptedData
+  } else {
+    return ''
+  }
+}
+
+// 使用私钥解密
+export const myJieMiFu = (encryptedData: string, privateKey: string): string | false => {
+  const decryptor = new JSEncrypt()
+  decryptor.setPrivateKey(privateKey)
+  return decryptor.decrypt(encryptedData)
+}
+
+// 自己的 私钥 签名
+export const signData = (data: string, privateKey: string): string | false => {
+  const encryptor = new JSEncrypt()
+  encryptor.setPrivateKey(privateKey)
+  try {
+    // 使用SHA256进行签名
+    const signature = encryptor.sign(data, CryptoJS.SHA256 as any, 'sha256')
+    return signature
+  } catch (error) {
+    console.error('签名失败:', error)
+    return false
+  }
+}
+
+// --------------我方生成公钥和私钥
+
+// const generateKeyPairWebCrypto = async (): Promise<{
+//   publicKey: string
+//   privateKey: string
+// }> => {
+//   try {
+//     // 生成密钥对
+//     const keyPair = await window.crypto.subtle.generateKey(
+//       {
+//         name: 'RSA-OAEP',
+//         modulusLength: 2048, // 密钥长度
+//         publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
+//         hash: { name: 'SHA-256' } // 哈希算法
+//       },
+//       true, // 是否可导出
+//       ['encrypt', 'decrypt'] // 密钥用途
+//     )
+
+//     // 导出公钥 (SPKI 格式)
+//     const exportedPublicKey = await window.crypto.subtle.exportKey(
+//       'spki',
+//       keyPair.publicKey
+//     )
+//     // 导出私钥 (PKCS#8 格式)
+//     const exportedPrivateKey = await window.crypto.subtle.exportKey(
+//       'pkcs8',
+//       keyPair.privateKey
+//     )
+
+//     // 将 ArrayBuffer 转换为 Base64 字符串
+//     const publicKeyBase64 = btoa(
+//       String.fromCharCode(...new Uint8Array(exportedPublicKey))
+//     )
+//     const privateKeyBase64 = btoa(
+//       String.fromCharCode(...new Uint8Array(exportedPrivateKey))
+//     )
+
+//     // 格式化为 PEM 格式(可选,许多库使用 PEM 格式)
+//     const publicKeyPem = `-----BEGIN PUBLIC KEY-----\n${publicKeyBase64}\n-----END PUBLIC KEY-----`
+//     const privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyBase64}\n-----END PRIVATE KEY-----`
+
+//     return { publicKey: publicKeyPem, privateKey: privateKeyPem }
+//   } catch (error) {
+//     console.error('生成密钥对时发生错误:', error)
+//     throw error
+//   }
+// }
+
+// // 使用示例
+// generateKeyPairWebCrypto()
+//   .then(({ publicKey, privateKey }) => {
+//     console.log('公钥:', publicKey)
+//     console.log('私钥:', privateKey)
+//     // 注意:在实际应用中,私钥应妥善保管,避免暴露在前端代码或客户端环境中。
+//   })
+//   .catch(error => {
+//     // 处理错误
+//   })

+ 2 - 1
tsconfig.json

@@ -2,6 +2,7 @@
   "extends": "./path.tsconfig.json",
   "compilerOptions": {
     "target": "es5",
+    "downlevelIteration": true, // 启用对迭代器的降级支持
     "lib": [
       "dom",
       "dom.iterable",
@@ -24,4 +25,4 @@
   "include": [
     "src"
   ]
-}
+}

+ 15 - 0
yarn.lock

@@ -2031,6 +2031,11 @@
   dependencies:
     "@types/node" "*"
 
+"@types/crypto-js@^4.2.2":
+  version "4.2.2"
+  resolved "https://registry.npmmirror.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea"
+  integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==
+
 "@types/eslint-scope@^3.7.3":
   version "3.7.4"
   resolved "https://registry.npmmirror.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz"
@@ -3684,6 +3689,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+crypto-js@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
+  integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
+
 crypto-random-string@^2.0.0:
   version "2.0.0"
   resolved "https://registry.npmmirror.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz"
@@ -6352,6 +6362,11 @@ jsdom@^16.6.0:
     ws "^7.4.6"
     xml-name-validator "^3.0.0"
 
+jsencrypt@^3.5.4:
+  version "3.5.4"
+  resolved "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.5.4.tgz#8db335ab164359449dd200d120f125f459476b25"
+  integrity sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==
+
 jsesc@^2.5.1:
   version "2.5.2"
   resolved "https://registry.npmmirror.com/jsesc/-/jsesc-2.5.2.tgz"