feat: 企业微信扫码登录插件 (#1364)

Co-authored-by: root <root@huahua.daoyoucloud.com>
Co-authored-by: TomyJan <TomyJan6@gmail.com>
Reviewed-on: daoyoucloud/tachybase#1364
Reviewed-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: huahua <1494133104@qq.com>
Co-committed-by: huahua <1494133104@qq.com>
This commit is contained in:
huahua 2024-07-23 01:10:52 +08:00 committed by sealday
parent 9ffd94155a
commit b6b94e200e
22 changed files with 533 additions and 26 deletions

View File

@ -1,6 +1,10 @@
{
"name": "@tachybase/plugin-dingtalk",
"version": "0.21.76",
"description": "dingding authentication.",
"keywords": [
"Authentication"
],
"main": "dist/server/index.js",
"devDependencies": {
"@alicloud/dingtalk": "^2.1.30",

View File

@ -0,0 +1,2 @@
/node_modules
/src

View File

@ -0,0 +1 @@
# @tachybase/plugin-work-wechat

View File

@ -0,0 +1,2 @@
export * from './dist/client';
export { default } from './dist/client';

View File

@ -0,0 +1 @@
module.exports = require('./dist/client/index.js');

View File

@ -0,0 +1,24 @@
{
"name": "@tachybase/plugin-work-wechat",
"version": "0.21.74",
"description": "work WeChat authentication.",
"keywords": [
"Authentication"
],
"main": "dist/server/index.js",
"dependencies": {
"@ant-design/icons": "^5.3.7",
"ahooks": "^3.8.0",
"antd": "5.19.1",
"axios": "^1.7.2",
"react-router-dom": "^6.25.1"
},
"peerDependencies": {
"@tachybase/actions": "workspace:*",
"@tachybase/auth": "workspace:*",
"@tachybase/client": "workspace:*",
"@tachybase/plugin-auth": "workspace:*",
"@tachybase/server": "workspace:*",
"@tachybase/test": "workspace:*"
}
}

View File

@ -0,0 +1,2 @@
export * from './dist/server';
export { default } from './dist/server';

View File

@ -0,0 +1 @@
module.exports = require('./dist/server/index.js');

View File

@ -0,0 +1,111 @@
import React, { useEffect } from 'react';
import { SchemaComponent, useAPIClient, useApp } from '@tachybase/client';
import { CopyOutlined, LoginOutlined } from '@ant-design/icons';
import { Button, Input, message, Typography } from 'antd';
import { useLocation } from 'react-router-dom';
import { useTranslation } from './locale';
export const AdminSettingsForm = () => {
const { t } = useTranslation();
const app = useApp();
const redirectUrl = React.useMemo(() => app.getApiUrl('workWeChat:redirect'), [app]);
const onCopy = (text) => {
navigator.clipboard.writeText(text), message.success(t('Copied'));
};
return (
<div>
<SchemaComponent
scope={{ t }}
schema={{
type: 'object',
properties: {
public: {
type: 'object',
properties: {
autoSignup: {
'x-decorator': 'FormItem',
type: 'boolean',
title: '{{t("Sign up automatically when the user does not exist")}}',
'x-component': 'Checkbox',
default: true,
},
},
},
workWeChat: {
type: 'object',
properties: {
agentId: {
'x-decorator': 'FormItem',
'x-component': 'Input',
type: 'string',
required: true,
title: '{{t("Agent ID")}}',
},
corpId: {
'x-decorator': 'FormItem',
'x-component': 'Input',
type: 'string',
required: true,
title: '{{t("Corp ID")}}',
},
corpSecret: {
'x-decorator': 'FormItem',
'x-component': 'Input',
type: 'string',
required: true,
title: '{{t("Corp Secret")}}',
},
},
},
},
}}
/>
<div>
<Typography.Title level={5} style={{ fontSize: '14px' }}>
{t('Redirect URL')}
<span style={{ marginLeft: '2px' }}>:</span>
</Typography.Title>
<Input value={redirectUrl} disabled={true} addonBefore={<CopyOutlined onClick={() => onCopy(redirectUrl)} />} />
</div>
</div>
);
};
export const SignInButton = ({ authenticator }) => {
const [href, setHref] = React.useState();
const { t } = useTranslation();
const api = useAPIClient();
const location = useLocation();
const urlSearchParams = new URLSearchParams(location.search);
const redirect = urlSearchParams.get('redirect');
useEffect(() => {
href && (window.location.href = href);
}, [href]);
useEffect(() => {
const authenticatorName = urlSearchParams.get('authenticator');
const error = urlSearchParams.get('error');
if (authenticatorName === authenticator.name && error) {
message.error(t(error));
return;
}
});
const onClick = async () => {
const result = await api.request({
method: 'post',
url: 'workWeChat:getAuthUrl',
headers: { 'X-Authenticator': authenticator.name },
data: { redirect: redirect },
});
const url = result?.data?.data?.url;
if (url) {
setHref(url);
}
};
return (
<Button block icon={<LoginOutlined />} shape="round" onClick={onClick}>
{authenticator.title ? t(authenticator.title) : t('Sign in via work WeChat')}
</Button>
);
};

View File

@ -0,0 +1,13 @@
import { Plugin } from '@tachybase/client';
import PluginAuthClient from '@tachybase/plugin-auth/client';
import { authType } from '../constants';
import { AdminSettingsForm, SignInButton } from './components';
export class PluginWorkWeChatClient extends Plugin {
async load() {
this.app.pm.get(PluginAuthClient).registerType(authType, { components: { SignInButton, AdminSettingsForm } });
}
}
export default PluginWorkWeChatClient;

View File

@ -0,0 +1,7 @@
import { useTranslation as useT } from '@tachybase/client';
import { namespace } from '../constants';
export function useTranslation() {
return useT([namespace, 'client'], { nsMode: 'fallback' });
}

View File

@ -0,0 +1,2 @@
export const authType = 'Work WeChat';
export const namespace = 'work';

View File

@ -0,0 +1,2 @@
export * from './server';
export { default } from './server';

View File

@ -0,0 +1,16 @@
export default {
"Sign in via work WeChat": "Sign in via Work WeChat",
"Agent ID": "Agent ID",
"Corp ID": "Corp ID",
"Corp Secret": "Corp Secret",
"WorkWeChat": "WorkWeChat",
"workWeChat": "workWeChat",
"Redirect URL": "Redirect URL",
"Please enter the authCode": "Please enter the authCode",
"Authorizing Login": "Authorizing Login",
"Sign up automatically when the user does not exist": "Sign up automatically when the user does not exist",
"User not found": "User not found",
"Failed to get mobile": "Failed to get mobile",
"Failed to get accessToken": "Failed to get accessToken",
"Copied": "Copied"
}

View File

@ -0,0 +1,16 @@
export default {
"Sign in via work WeChat": "企业微信登录",
"Agent ID": "Agent ID",
"Corp ID": "企业 ID",
"Corp Secret": "企业 Secret",
"WorkWeChat": "企业微信",
"workWeChat": "企业微信",
"Redirect URL": "回调地址",
"Please enter the authCode": "请输入authCode",
"Authorizing Login": "授权登录中...",
"Sign up automatically when the user does not exist": "用户不存在时自动注册",
"User not found": "用户不存在",
"Failed to get mobile": "未获取到手机号",
"Failed to get accessToken": "未获取到accessToken",
"Copied": "已复制"
}

View File

@ -0,0 +1,40 @@
import { Context, Next } from '@tachybase/actions';
import { AppSupervisor } from '@tachybase/server';
export const getAuthUrl = async (ctx: Context, next: Next) => {
const { redirect } = ctx.action.params.values;
const url = await ctx.auth.getAuthUrl(redirect);
ctx.body = { url };
await next();
};
export const redirect = async (ctx: Context, next: Next) => {
const { code, state } = ctx.request.query;
// 如果 state 是数组,取第一个元素
const stateString = Array.isArray(state) ? state[0] : state;
const search = new URLSearchParams(stateString);
const authenticator = search.get('name');
const appName = search.get('app');
const redirect = search.get('redirect') || '/admin';
let prefix = process.env.APP_PUBLIC_PATH || '';
if (appName && appName !== 'main') {
const appSupervisor = AppSupervisor.getInstance();
if (appSupervisor?.runningMode !== 'single') {
prefix += `apps/${appName}`;
}
}
const auth = await ctx.app.authManager.get(authenticator, ctx);
if (prefix.endsWith('/')) {
prefix = prefix.slice(0, -1);
}
try {
const { token } = await auth.signIn();
ctx.redirect(`${prefix}${redirect}?authenticator=${authenticator}&token=${token}`);
} catch (error) {
ctx.logger.error('Work auth error', { error });
ctx.redirect(`${prefix}/signin?redirect=${redirect}&authenticator=${authenticator}&error=${error.message}`);
}
await next();
};

View File

@ -0,0 +1 @@
export { default } from './plugin';

View File

@ -0,0 +1,52 @@
import { Gateway, Plugin } from '@tachybase/server';
import { authType } from '../constants';
import { getAuthUrl, redirect } from './actions/work';
import { WorkAuth } from './work-auth';
export class PluginWorkWeChatServer extends Plugin {
async afterAdd() {}
async beforeLoad() {}
async load() {
this.app.authManager.registerTypes(authType, {
auth: WorkAuth,
});
this.app.resourcer.define({
name: 'workWeChat',
actions: {
getAuthUrl,
redirect,
},
});
this.app.acl.allow('workWeChat', '*', 'public');
Gateway.getInstance().addAppSelectorMiddleware(async (ctx, next) => {
const { req } = ctx;
const url = new URL(req.url, `http://${req.headers.host}`);
const params = url.searchParams;
const state = params.get('state');
if (state) {
const search = new URLSearchParams(state);
const appName = search.get('app');
if (appName) {
ctx.resolvedAppName = appName;
}
}
await next();
});
}
async install() {}
async afterEnable() {}
async afterDisable() {}
async remove() {}
}
export default PluginWorkWeChatServer;

View File

@ -0,0 +1,81 @@
import { AuthConfig, BaseAuth } from '@tachybase/auth';
import { AuthModel } from '@tachybase/plugin-auth';
import { namespace } from '../constants';
import { WorkClient } from './work-client.js';
export { Model } from '@tachybase/database';
export class WorkAuth extends BaseAuth {
constructor(config: AuthConfig) {
const { ctx } = config;
super({
...config,
userCollection: ctx.db.getCollection('users'),
});
}
async validate() {
const ctx = this.ctx;
const { code } = ctx.request.query || {};
const codeString = Array.isArray(code) ? code[0] : code;
if (!codeString) {
ctx.throw(401, ctx.t('Please enter the authCode', { ns: namespace }));
}
const workClient = new WorkClient({
clientId: this.options?.work?.corpId,
clientSecret: this.options?.work?.corpSecret,
ctx: this.ctx,
});
const accessToken = await workClient.getAccessToken();
if (!accessToken) {
ctx.throw(401, ctx.t('Failed to get accessToken', { ns: namespace }));
}
const userInfo = await workClient.getUserInfo(accessToken, codeString);
const { telephone, name, userid } = userInfo;
if (!telephone) {
ctx.throw(401, ctx.t('Failed to get telephone', { ns: namespace }));
}
const authenticator = this.authenticator;
let user = await authenticator.findUser(telephone);
if (user) {
return user;
}
user = await this.userRepository.findOne({
filter: { phone: telephone },
});
if (user) {
await authenticator.addUser(user.id, {
through: {
uuid: telephone,
},
});
return user;
}
const { autoSignup } = this.options?.public || {};
if (!autoSignup) {
ctx.throw(401, ctx.t('User not found', { ns: namespace }));
}
return await authenticator.newUser(telephone, {
nickname: name ?? null,
phone: telephone ?? null,
});
}
async getAuthUrl(redirect) {
const clientId = this.options?.work?.corpId;
const app = this.ctx.app.name;
// const redirectUrl = encodeURIComponent('https://hua.dev.daoyoucloud.com/api/workWeChat:redirect');
const redirectUrl = encodeURIComponent(`${this.ctx.host}${process.env.API_BASE_PATH}workWeChat:redirect`);
// TODO: 如果后续有登录后绑定的场景,服务端需要校验 state
const state = encodeURIComponent(
encodeURIComponent(`redirect=${redirect}&app=${app}&name=${this.ctx.headers['x-authenticator']}`),
);
const url = `https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=${clientId}&agentid=${this.options?.work?.agentId}&redirect_uri=${redirectUrl}&state=${state}`;
return url;
}
}

View File

@ -0,0 +1,64 @@
import axios from 'axios';
interface Config {
clientId: string;
clientSecret: string;
ctx?: any;
}
export class WorkClient {
config: Config;
ctx: any;
accessToken: string = '';
tokenExpiresAt: number = 0;
constructor(config: Config) {
this.config = config;
this.ctx = config?.ctx;
}
private async fetchNewAccessToken(): Promise<void> {
try {
const response = await axios.get(
`https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${this.config.clientId}&corpsecret=${this.config.clientSecret}`,
);
const data = response.data;
this.accessToken = data.access_token;
this.tokenExpiresAt = Date.now() + data.expires_in * 1000;
} catch (error) {
console.error('获取 access_token 失败:', error);
throw error;
}
}
public async getAccessToken(): Promise<string> {
if (!this.accessToken || Date.now() > this.tokenExpiresAt) {
await this.fetchNewAccessToken();
}
return this.accessToken;
}
async getUserInfo(accessToken: string, code: string): Promise<any> {
try {
const response = await axios.get(
`https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=${accessToken}&code=${code}`,
);
const userId = response.data.UserId;
if (userId) {
const userDetailResponse = await axios.post(
`https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=${accessToken}&userid=${userId}`,
);
return userDetailResponse.data;
} else {
this.ctx?.log.debug(`get userInfo error message: ${response.data.errmsg}`);
return {};
}
} catch (err) {
if (err.response?.data?.errmsg) {
this.ctx?.log.debug(`get userInfo error message: ${err.response.data.errmsg}`);
}
return {};
}
}
}

View File

@ -4024,6 +4024,42 @@ importers:
specifier: ^0.6.2
version: 0.6.2
packages/plugins/@tachybase/plugin-work-wechat:
dependencies:
'@ant-design/icons':
specifier: ^5.3.7
version: 5.3.7(react-dom@18.3.1)(react@18.3.1)
'@tachybase/actions':
specifier: workspace:*
version: link:../../../core/actions
'@tachybase/auth':
specifier: workspace:*
version: link:../../../core/auth
'@tachybase/client':
specifier: workspace:*
version: link:../../../core/client
'@tachybase/plugin-auth':
specifier: workspace:*
version: link:../plugin-auth
'@tachybase/server':
specifier: workspace:*
version: link:../../../core/server
'@tachybase/test':
specifier: workspace:*
version: link:../../../core/test
ahooks:
specifier: ^3.8.0
version: 3.8.0(react@18.3.1)
antd:
specifier: 5.19.1
version: 5.19.1(react-dom@18.3.1)(react@18.3.1)
axios:
specifier: ^1.7.2
version: 1.7.2
react-router-dom:
specifier: ^6.25.1
version: 6.25.1(react-dom@18.3.1)(react@18.3.1)
packages/plugins/@tachybase/plugin-workflow:
dependencies:
'@tachybase/actions':
@ -4847,7 +4883,7 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-router: ^5.0.0 || ^6.0.0
dependencies:
ahooks: 3.7.8(react@18.3.1)
ahooks: 3.7.11(react@18.3.1)
query-string: 6.14.1
react: 18.3.1
react-router: 6.21.1(react@18.3.1)
@ -5158,10 +5194,10 @@ packages:
react-dom: '>=17.0.0'
dependencies:
'@ant-design/cssinjs': 1.18.2(react-dom@18.3.1)(react@18.3.1)
'@babel/runtime': 7.24.0
'@babel/runtime': 7.24.7
'@ctrl/tinycolor': 3.6.1
antd: 5.19.1(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.41.0(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
swr: 2.2.4(react@18.3.1)
@ -5176,12 +5212,12 @@ packages:
dependencies:
'@ant-design/icons': 5.3.7(react-dom@18.3.1)(react@18.3.1)
'@ant-design/pro-provider': 2.13.5(antd@5.19.1)(react-dom@18.3.1)(react@18.3.1)
'@babel/runtime': 7.24.0
'@babel/runtime': 7.24.7
antd: 5.19.1(react-dom@18.3.1)(react@18.3.1)
classnames: 2.5.1
dayjs: 1.11.10
lodash.merge: 4.6.2
rc-util: 5.41.0(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
safe-stable-stringify: 2.4.3
@ -7964,7 +8000,7 @@ packages:
outdent: 0.5.0
prettier: 2.8.8
resolve-from: 5.0.0
semver: 7.6.2
semver: 7.6.0
dev: true
/@changesets/assemble-release-plan@6.0.0:
@ -7975,7 +8011,7 @@ packages:
'@changesets/get-dependents-graph': 2.0.0
'@changesets/types': 6.0.0
'@manypkg/get-packages': 1.1.3
semver: 7.6.2
semver: 7.6.0
dev: true
/@changesets/changelog-git@0.2.0:
@ -8047,7 +8083,7 @@ packages:
'@manypkg/get-packages': 1.1.3
chalk: 2.4.2
fs-extra: 7.0.1
semver: 7.6.2
semver: 7.6.0
dev: true
/@changesets/get-release-plan@4.0.0:
@ -11115,12 +11151,12 @@ packages:
react: '>=16.9.0'
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.24.5
'@babel/runtime': 7.24.7
'@rc-component/portal': 1.1.2(react-dom@18.3.1)(react@18.3.1)
classnames: 2.5.1
rc-motion: 2.9.1(react-dom@18.3.1)(react@18.3.1)
rc-resize-observer: 1.4.0(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.41.0(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: true
@ -13254,7 +13290,7 @@ packages:
'@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5)
eslint: 8.55.0
eslint-scope: 5.1.1
semver: 7.6.2
semver: 7.6.0
transitivePeerDependencies:
- supports-color
- typescript
@ -14416,6 +14452,24 @@ packages:
screenfull: 5.2.0
tslib: 2.6.2
/ahooks@3.8.0(react@18.3.1):
resolution: {integrity: sha512-M01m+mxLRNNeJ/PCT3Fom26UyreTj6oMqJBetUrJnK4VNI5j6eMA543Xxo53OBXn6XibA2FXKcCCgrT6YCTtKQ==}
engines: {node: '>=8.0.0'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.24.7
dayjs: 1.11.10
intersection-observer: 0.12.2
js-cookie: 2.2.1
lodash: 4.17.21
react: 18.3.1
react-fast-compare: 3.2.2
resize-observer-polyfill: 1.5.1
screenfull: 5.2.0
tslib: 2.6.3
dev: false
/ajv-formats@1.6.1(ajv@7.2.4):
resolution: {integrity: sha512-4CjkH20If1lhR5CGtqkrVg3bbOtFEG80X9v6jDOIUhbzzbB+UzPBGy8GQhUNVZ0yvMHdMpawCOcy5ydGMsagGQ==}
peerDependencies:
@ -15140,6 +15194,16 @@ packages:
transitivePeerDependencies:
- debug
/axios@1.7.2:
resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==}
dependencies:
follow-redirects: 1.15.6(debug@4.3.5)
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
dev: false
/b4a@1.6.4:
resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==}
dev: true
@ -25826,9 +25890,9 @@ packages:
react: '>=16.0.0'
react-dom: '>=16.0.0'
dependencies:
'@babel/runtime': 7.24.5
'@babel/runtime': 7.24.7
classnames: 2.5.1
rc-util: 5.41.0(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: true
@ -26090,12 +26154,12 @@ packages:
react: '*'
react-dom: '*'
dependencies:
'@babel/runtime': 7.24.0
'@babel/runtime': 7.24.7
'@rc-component/trigger': 1.18.3(react-dom@18.3.1)(react@18.3.1)
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.3.1)(react@18.3.1)
rc-overflow: 1.3.2(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.38.1(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1)
rc-virtual-list: 3.11.3(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@ -26108,12 +26172,12 @@ packages:
react: '*'
react-dom: '*'
dependencies:
'@babel/runtime': 7.24.5
'@babel/runtime': 7.24.7
'@rc-component/trigger': 2.1.1(react-dom@18.3.1)(react@18.3.1)
classnames: 2.5.1
rc-motion: 2.9.1(react-dom@18.3.1)(react@18.3.1)
rc-overflow: 1.3.2(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.41.0(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1)
rc-virtual-list: 3.11.4(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@ -26315,10 +26379,10 @@ packages:
react: '*'
react-dom: '*'
dependencies:
'@babel/runtime': 7.24.0
'@babel/runtime': 7.24.7
classnames: 2.5.1
rc-motion: 2.9.0(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.38.1(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1)
rc-virtual-list: 3.11.3(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@ -26331,10 +26395,10 @@ packages:
react: '*'
react-dom: '*'
dependencies:
'@babel/runtime': 7.24.5
'@babel/runtime': 7.24.7
classnames: 2.5.1
rc-motion: 2.9.1(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.41.0(react-dom@18.3.1)(react@18.3.1)
rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1)
rc-virtual-list: 3.14.2(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@ -26400,7 +26464,7 @@ packages:
react: '>=16.9.0'
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.24.1
'@babel/runtime': 7.24.7
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-is: 18.3.1
@ -26411,10 +26475,11 @@ packages:
react: '>=16.9.0'
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.24.0
'@babel/runtime': 7.24.7
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-is: 18.3.1
dev: true
/rc-util@5.42.0(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-uxj2fkMe++/A3CTNagEljdTjZJHVFNH5EZcK9D4YAtWWTdEMglRE4VFtd0psIPUBIY+lSdqwVcIrR1oQMR07vw==}
@ -26826,7 +26891,7 @@ packages:
react-native:
optional: true
dependencies:
'@babel/runtime': 7.24.0
'@babel/runtime': 7.24.7
'@types/react-redux': 7.1.33
hoist-non-react-statics: 3.3.2
loose-envify: 1.4.0
@ -27103,7 +27168,7 @@ packages:
/redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
dependencies:
'@babel/runtime': 7.24.0
'@babel/runtime': 7.24.7
/reflect-metadata@0.1.14:
resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==}
@ -28151,7 +28216,7 @@ packages:
git-hooks-list: 3.1.0
globby: 13.2.2
is-plain-obj: 4.1.0
semver: 7.6.2
semver: 7.6.0
sort-object-keys: 1.1.3
dev: true