feat: 微信公众号登录插件-未重定向 (#1405)

Reviewed-on: daoyoucloud/tachybase#1405
Reviewed-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: luliangqiang <2650321653@qq.com>
Co-committed-by: luliangqiang <2650321653@qq.com>
This commit is contained in:
luliangqiang 2024-07-30 11:08:16 +08:00 committed by sealday
parent 7e02b5a71c
commit 9261719589
8 changed files with 562 additions and 90 deletions

View File

@ -1,12 +1,21 @@
{
"name": "@tachybase/plugin-wechat-official-account",
"version": "0.21.79",
"version": "0.21.73",
"keywords": [
"Authentication"
],
"main": "dist/server/index.js",
"dependencies": {
"@ant-design/icons": "^5.3.7",
"@types/xml2js": "^0.4.14",
"antd": "^5.18.3",
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"crypto": "^1.0.1",
"jsonwebtoken": "^8.5.1",
"koa-bodyparser": "^4.3.0",
"qrcode.react": "^3.1.0",
"react-router-dom": "^6.11.2",
"xml2js": "^0.6.2"
},

View File

@ -0,0 +1,139 @@
// --4.1 直接插入缓存
import React, { useEffect, useState } from 'react';
import { SchemaComponent, useAPIClient, useApp } from '@tachybase/client';
import { CopyOutlined, LoginOutlined } from '@ant-design/icons';
import { Button, Input, message, Modal, Spin, Typography } from 'antd';
import { useLocation } from 'react-router-dom';
import { useTranslation } from './locale';
export const AdminSettingsForm = () => {
const { t } = useTranslation();
const app = useApp();
const redirectUrl = 'https://lu.dev.daoyoucloud.com/api/wechat@handleOAuthCallback'; // 设置回调url
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,
},
},
},
wechat: {
type: 'object',
properties: {
appId: {
'x-decorator': 'FormItem',
'x-component': 'Input',
type: 'string',
required: true,
title: '{{t("WeChat AppID")}}',
},
appSecret: {
'x-decorator': 'FormItem',
'x-component': 'Input',
type: 'string',
required: true,
title: '{{t("WeChat AppSecret")}}',
},
},
},
},
}}
/>
<div>
<Typography.Title level={5} style={{ fontSize: '14px' }}>
{t('Redirect URL')}:
</Typography.Title>
<Input value={redirectUrl} disabled addonBefore={<CopyOutlined onClick={() => onCopy(redirectUrl)} />} />
</div>
</div>
);
};
export const SignInButton = ({ authenticator }) => {
const [href, setHref] = useState();
const [qrCodeVisible, setQrCodeVisible] = useState(false);
const [qrCodeUrl, setQrCodeUrl] = useState('');
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const api = useAPIClient();
const location = useLocation();
const urlSearchParams = new URLSearchParams(location.search);
const redirect = urlSearchParams.get('redirect');
useEffect(() => {
if (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));
}
}, [authenticator.name, t, urlSearchParams]);
const onClick = async () => {
setLoading(true);
try {
const { data } = await api.request({
method: 'post',
url: 'https://lu.dev.daoyoucloud.com/api/wechat@generateQrCode', // 生成二维码
headers: { 'X-Authenticator': authenticator.name },
data: { redirect },
});
console.log('data.data.data.url:', data.data.data.url);
const url = data.data.data.url;
if (url) {
setQrCodeUrl(url);
setQrCodeVisible(true);
} else {
console.error('未能生成二维码URL');
}
} catch (error) {
message.error(t('Failed to generate QR code'));
console.error('二维码生成失败:', error);
} finally {
setLoading(false);
}
};
// 显示二维码
return (
<>
<Button block icon={<LoginOutlined />} shape="round" onClick={onClick} loading={loading}>
{authenticator.title || 'WeChat 公众号登录'}
</Button>
<Modal
title="Scan QR Code to Login"
style={{ textAlign: 'center' }}
visible={qrCodeVisible}
footer={null}
onCancel={() => setQrCodeVisible(false)}
>
{loading ? <Spin /> : <img src={qrCodeUrl} alt="QR Code" style={{ width: '100%' }} />}
</Modal>
</>
);
};

View File

@ -1,17 +1,13 @@
import React from 'react';
import { Plugin, useRequest } from '@tachybase/client';
import { Plugin } from '@tachybase/client';
import PluginAuthClient from '@tachybase/plugin-auth/client';
import { CustomAuthLayout } from './AuthLayout';
import { authType } from '../constants';
import { AdminSettingsForm, SignInButton } from './components';
export class PluginReplacePageClient extends Plugin {
async afterAdd() {
// await this.app.pm.add()
export class PluginWeCahtOfficialAccount extends Plugin {
async load() {
this.app.pm.get(PluginAuthClient).registerType(authType, { components: { SignInButton, AdminSettingsForm } });
}
async beforeLoad() {}
// You can get and modify the app instance here
async load() {}
}
export default PluginReplacePageClient;
export default PluginWeCahtOfficialAccount;

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 = '微信公众号';
export const namespace = '微信公众号';

View File

@ -0,0 +1,39 @@
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;
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

@ -1,22 +1,78 @@
// --10、解决数据库注册插入问题
import crypto from 'crypto';
import { BasicAuth } from '@tachybase/plugin-auth';
import { Plugin } from '@tachybase/server';
import axios from 'axios';
import xml2js from 'xml2js';
import { authType } from '../constants';
export class PluginReplacePageServer extends Plugin {
// 从数据库查询cofig表
async config(): Promise<any> {
// 从数据库查询config表
async config() {
const repo = this.app.db.getRepository('config');
const config = (await repo.findById(1)) as any;
return config.get(); // get()返回具体的data
const config = await repo.findById(1);
return config.get();
}
// 从数据库查询message表
async message(): Promise<any[]> {
async message() {
const repo = this.app.db.getRepository('message');
const messages = (await repo.find()) as any;
return messages; // 返回数组Type
const messages = await repo.find();
return messages;
}
// 从数据库查询menu表
async menu() {
const repo = this.app.db.getRepository('menu');
const menus = await repo.find();
// 将数据库中的数据构建成菜单结构
const menuStructure = { button: [] };
menus.forEach((menu) => {
if (!menu.button) {
// 一级菜单
if (menuStructure.button.length < 3) {
if (Buffer.byteLength(menu.name, 'utf8') <= 16) {
menuStructure.button.push({
type: menu.type,
name: menu.name,
key: menu.key || undefined,
url: menu.url || undefined,
sub_button: [],
});
} else {
console.warn(`一级菜单名称 "${menu.name}" 超过了 16 个字节的限制`);
}
} else {
console.warn('超过了一级菜单的数量限制');
}
} else {
// 二级菜单
const parentMenu = menuStructure.button.find((btn) => btn.name === menu.button);
if (parentMenu) {
if (parentMenu.sub_button.length < 5) {
if (Buffer.byteLength(menu.name, 'utf8') <= 40) {
parentMenu.sub_button.push({
type: menu.type,
name: menu.name,
key: menu.key || undefined,
url: menu.url || undefined,
});
} else {
console.warn(`二级菜单名称 "${menu.name}" 超过了 40 个字节的限制`);
}
} else {
console.warn('超过了二级菜单的数量限制');
}
}
}
});
console.log('-------------', JSON.stringify(menuStructure, null, 2));
return menuStructure;
}
// 验证Wechat平台的请求签名
@ -35,8 +91,12 @@ export class PluginReplacePageServer extends Plugin {
const { appid, appsecret } = await this.config(); // 使用config方法 查询config表
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${appsecret}`;
console.log(`Fetching access token from URL: ${url}`);
console.log(`App ID: ${appid}, App Secret: ${appsecret}`);
try {
const response = await axios.get(url);
console.log('Access token response:', response.data);
return response.data.access_token;
} catch (error) {
console.error('Error getting access token:', error);
@ -44,9 +104,33 @@ export class PluginReplacePageServer extends Plugin {
}
}
// 生成OAuth授权URL
async generateAuthUrl() {
const { appid } = await this.config(); // 获取appid
const redirectUri = encodeURIComponent('https://lu.dev.daoyoucloud.com/api/wechat@handleOAuthCallback');
const scope = 'snsapi_userinfo'; // 获取用户信息的权限
const state = 'STATE'; // 自定义的state参数
const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
return authUrl;
}
// 生成二维码URL
async generateQrCode() {
const authUrl = await this.generateAuthUrl();
if (!authUrl) {
console.error('Failed to generate OAuth URL');
return null;
}
const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(authUrl)}&size=300x300`;
return qrCodeUrl;
}
// 创建菜单
async createCustomMenu() {
const accessToken = await this.getAccessToken(); // 获取Wechat Token
const menu = await this.menu(); // 获取菜单数据
if (!accessToken) {
console.error('Failed to get access token');
@ -54,35 +138,6 @@ export class PluginReplacePageServer extends Plugin {
}
const url = `https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${accessToken}`;
const menu = {
button: [
{
type: 'view',
name: '音乐',
url: 'https://music.163.com/', // 确保这个 URL 不超过 256 个字符
},
{
type: 'view',
name: '官网',
url: 'https://daoyoucloud.com/',
},
{
name: '服务',
sub_button: [
{
type: 'view',
name: '正在开发中...',
url: 'https://daoyoucloud.com/coming-soon',
},
{
type: 'view',
name: '开发者管理',
url: 'https://lu.dev.daoyoucloud.com/',
},
],
},
],
};
// 若通过accessToken验证则发送Wechat请求创建菜单
try {
@ -93,7 +148,7 @@ export class PluginReplacePageServer extends Plugin {
}
}
// Wechat从用户这里获取签名 若通过则允许后续交互
// Wechat从用户这里获取签名若通过则允许后续交互
async get(ctx) {
const { query } = ctx.request;
if (await this.checkSignature(query)) {
@ -114,10 +169,9 @@ export class PluginReplacePageServer extends Plugin {
}
const messages = await this.message(); // 调用当前对象的 message方法
// const keywordsList = keywords.map(keyword => keyword.keywords).join(`${keywords}`);
const messagesList = messages.map((message, index) => `${index + 1}${message.key}`).join('\n');
const messagesList = messages.map((message) => `${message.id}${message.key}`).join('\n');
let replyContent = `您好,欢迎关注道有云网络科技有限公司!您可以输入下面关键字来获取您要了解的信息!\n(输入序号即可)\n${messagesList}`; // 默认回复内容
let replyContent = `您好,欢迎关注道有云网络科技有限公司!您可以输入下面关键字来获取您要了解的信息!\n(输入序号或关键字)\n${messagesList}`;
// 解析 POST 请求的 XML 格式消息体
xml2js.parseString(body, { explicitArray: false }, (err, result) => {
@ -131,14 +185,28 @@ export class PluginReplacePageServer extends Plugin {
const msg = result.xml;
let replyMessage; // 处理用户输入后的回复结果
if (msg.MsgType === 'text') {
if (msg.MsgType === 'event' && msg.Event === 'SCAN') {
// 处理扫码事件
replyContent = '扫码成功,欢迎您!';
replyMessage = `
<xml>
<ToUserName><![CDATA[${msg.FromUserName}]]></ToUserName>
<FromUserName><![CDATA[${msg.ToUserName}]]></FromUserName>
<CreateTime>${Math.floor(Date.now() / 1000)}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[${replyContent}]]></Content>
</xml>
`;
} else if (msg.MsgType === 'text') {
// 处理文本信息
const valueById = messages.find((message) => msg.Content.includes(message.id.toString()));
const valueByKey = messages.find((message) => msg.Content.includes(message.key));
// 匹配用户输入的关键字和message表里的messages字段是否一致一致则 filter()出一个数组
const values = messages.filter((message) => msg.Content.includes(message.id));
if (values?.length > 0) {
// 问号? 异常处理
replyContent = values[0].value;
// 异常处理
if (valueById) {
replyContent = valueById.value;
} else if (valueByKey) {
replyContent = valueByKey.value;
}
replyMessage = `
@ -180,36 +248,213 @@ export class PluginReplacePageServer extends Plugin {
});
}
// 加载
// 处理OAuth回调
async handleOAuthCallback(ctx) {
const { code } = ctx.query;
console.log('code---:', code);
if (!code) {
ctx.status = 400;
ctx.body = 'Authorization code not found';
return;
}
try {
const { appid, appsecret } = await this.config();
console.log('appid---:', appid);
console.log('appsecret---:', appsecret);
// 获取 access_token 和 openid
const tokenUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=${appsecret}&code=${code}&grant_type=authorization_code`;
const tokenResponse = await axios.get(tokenUrl);
const { access_token, openid } = tokenResponse.data;
console.log('access_token---:', access_token, '\n openid---:', openid);
if (!access_token || !openid) {
throw new Error('Failed to get access token or openid');
}
const userInfoUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=zh_CN`;
// 通过tokenUrl 获取用户message
const userInfoResponse = await axios.get(userInfoUrl);
// 用户信息
const userInfo = userInfoResponse.data;
console.log('用户信息:', userInfo);
// 检查数据库中是否存在该用户
const userRepo = this.app.db.getRepository('users');
const existingUser = await userRepo.findOne({
filter: {
username: openid,
},
});
if (!existingUser) {
// 如果用户不存在,则插入新用户
await userRepo.create({
values: {
username: openid.substring(0, 6),
password: openid,
nickname: userInfo.nickname,
},
});
console.log('新用户已创建:', userInfo.nickname);
} else {
// 如果用户存在,直接登录
console.log('用户已存在:', existingUser.nickname);
}
// 这里可以设置登录态或返回登录成功的信息
ctx.body = '完成注册,登录成功!';
// 返回手机端页面
// ctx.redirect('https://lu.dev.daoyoucloud.com/admin/4tr7gnzlwni');
} catch (error) {
console.error('Error handling OAuth callback:', error);
ctx.status = 500;
ctx.body = 'Server Error';
// ctx.body = '您已存在账户,登录成功!';
}
}
// load() 生命周期函数,在插件加载时执行
async load() {
// load() 生命周期函数,在插件加载时执行
// 监听 message 表的 afterCreate 事件
this.app.db.on('message.afterCreate', this.handleMessageCreate.bind(this));
// 监听 menu 表的 afterUpdate 事件
this.app.db.on('menu.afterUpdate', this.handleMenuUpdate.bind(this));
// 注册认证(前端显示按钮)
this.app.authManager.registerTypes(authType, {
auth: BasicAuth,
});
// 定义资源动作
this.app.resourcer.define({
name: 'wechat',
actions: {
// 测试接口
test: async (ctx) => {
ctx.withoutDataWrapping = true;
ctx.body = 'Hello World!';
console.log('Hello World!');
},
// 创建菜单接口
createMenu: async (ctx) => {
await this.createCustomMenu();
console.log('Custom menu created successfully.');
ctx.body = 'success';
},
// Wechat 消息处理接口/服务器URL配置接口
handler: async (ctx) => {
ctx.withoutDataWrapping = true;
if (ctx.request.method.toLowerCase() === 'get') {
//toLowerCase()转换字符串为小写
await this.get(ctx);
} else {
await this.post(ctx);
}
},
// 生成二维码接口
generateQrCode: async (ctx) => {
const qrCodeUrl = await this.generateQrCode();
if (qrCodeUrl) {
ctx.body = { data: { url: qrCodeUrl } }; // 确保返回的数据格式正确
console.log(`我是生成二维码接口的URL: ${qrCodeUrl}`);
} else {
ctx.status = 500;
ctx.body = 'Failed to generate QR code';
}
},
// 处理OAuth回调接口
handleOAuthCallback: async (ctx) => {
await this.handleOAuthCallback(ctx);
},
},
});
this.app.acl.allow('wechat', '*', 'public'); // 实体名称,允许所有资源,公共级别访问
this.app.acl.allow('wechat', '*', 'public');
}
// 处理 message 表的 afterCreate 事件
async handleMessageCreate(model, options) {
const { id, key, value } = model.get();
console.log(`New message created: ${key} - ${value}`);
// 获取所有关注公众号的用户 ID
const userIds = await this.getAllFollowers();
console.log('Follower IDs:', userIds);
// 推送消息content
await this.pushMessageToFollowers(userIds, `目前系统提示已经更新,大家可以使用新的关键字 "${key}" 来获取信息!`);
}
// 处理 menu 表的 afterUpdate 事件
async handleMenuUpdate(model, options) {
const { id, name } = model.get();
console.log(`Menu item updated: ${name} (ID: ${id})`);
// 获取所有关注公众号的用户 ID
const userIds = await this.getAllFollowers();
console.log('Follower IDs:', userIds);
// 推送消息content
await this.pushMessageToFollowers(userIds, '用户你好,菜单项已经更新');
}
// 获取用户列表
async getAllFollowers() {
const accessToken = await this.getAccessToken();
if (!accessToken) {
console.error('Failed to get access token');
return [];
}
const url = `https://api.weixin.qq.com/cgi-bin/user/get?access_token=${accessToken}`;
try {
const response = await axios.get(url);
if (response.data.errcode) {
console.error(`Error getting followers: ${response.data.errmsg}`);
return [];
}
console.log('返回用户ID数组----');
return response.data.data.openid; // 返回关注者的OpenID数组
} catch (error) {
console.error('Error getting followers:', error);
return [];
}
}
// 给用户发送消息
async pushMessageToFollowers(userIds, message) {
const accessToken = await this.getAccessToken();
if (!accessToken) {
console.error('Failed to get access token');
return;
}
const url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`;
for (const userId of userIds) {
const data = {
touser: userId,
msgtype: 'text',
text: {
content: message,
},
};
// 调用Wechat API对用户发送消息
try {
const response = await axios.post(url, data);
if (response.data.errcode) {
console.error(`Error pushing message to user ${userId}: ${response.data.errmsg}`);
} else {
console.log(`Message pushed to user ${userId} successfully.`);
}
} catch (error) {
console.error(`Error pushing message to user ${userId}:`, error);
}
}
}
}

View File

@ -4045,6 +4045,9 @@ importers:
packages/plugins/@tachybase/plugin-wechat-official-account:
dependencies:
'@ant-design/icons':
specifier: ^5.3.7
version: 5.3.7(react-dom@18.3.1)(react@18.3.1)
'@tachybase/client':
specifier: workspace:*
version: link:../../../core/client
@ -4063,12 +4066,27 @@ importers:
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
bcrypt:
specifier: ^5.1.1
version: 5.1.1
body-parser:
specifier: ^1.20.2
version: 1.20.2
crypto:
specifier: ^1.0.1
version: 1.0.1
jsonwebtoken:
specifier: ^8.5.1
version: 8.5.1
koa-bodyparser:
specifier: ^4.3.0
version: 4.4.1
qrcode.react:
specifier: ^3.1.0
version: 3.1.0(react@18.3.1)
react-router-dom:
specifier: ^6.11.2
version: 6.25.1(react-dom@18.3.1)(react@18.3.1)
@ -14770,7 +14788,6 @@ packages:
/are-we-there-yet@2.0.0:
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
engines: {node: '>=10'}
deprecated: This package is no longer supported.
dependencies:
delegates: 1.0.0
readable-stream: 3.6.2
@ -15303,6 +15320,18 @@ packages:
tweetnacl: 0.14.5
dev: true
/bcrypt@5.1.1:
resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==}
engines: {node: '>= 10.0.0'}
requiresBuild: true
dependencies:
'@mapbox/node-pre-gyp': 1.0.11
node-addon-api: 5.1.0
transitivePeerDependencies:
- encoding
- supports-color
dev: false
/bessel@1.0.2:
resolution: {integrity: sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw==}
engines: {node: '>=0.8'}
@ -15795,7 +15824,7 @@ packages:
dev: false
/call-bind@1.0.7:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==, tarball: https://registry.npmmirror.com/call-bind/-/call-bind-1.0.7.tgz}
engines: {node: '>= 0.4'}
dependencies:
es-define-property: 1.0.0
@ -16214,7 +16243,7 @@ packages:
engines: {node: '>=0.10.0'}
/co-body@6.1.0:
resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==}
resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==, tarball: https://registry.npmmirror.com/co-body/-/co-body-6.1.0.tgz}
dependencies:
inflation: 2.1.0
qs: 6.12.3
@ -16747,7 +16776,7 @@ packages:
toggle-selection: 1.0.6
/copy-to@2.0.1:
resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==}
resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==, tarball: https://registry.npmmirror.com/copy-to/-/copy-to-2.0.1.tgz}
/core-js-compat@3.34.0:
resolution: {integrity: sha512-4ZIyeNbW/Cn1wkMMDy+mvrRUxrwFNjKwbhCfQpDd+eLgYipDqp8oGFGtLmhh18EDPKA0g3VUBYOxQGGwvWLVpA==}
@ -16987,6 +17016,11 @@ packages:
engines: {node: '>=8'}
dev: false
/crypto@1.0.1:
resolution: {integrity: sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==}
deprecated: This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.
dev: false
/css-blank-pseudo@3.0.3(postcss@8.4.39):
resolution: {integrity: sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==}
engines: {node: ^12 || ^14 || >=16}
@ -17743,7 +17777,7 @@ packages:
dev: false
/define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==, tarball: https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz}
engines: {node: '>= 0.4'}
dependencies:
es-define-property: 1.0.0
@ -18356,13 +18390,13 @@ packages:
which-typed-array: 1.1.15
/es-define-property@1.0.0:
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==, tarball: https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.0.tgz}
engines: {node: '>= 0.4'}
dependencies:
get-intrinsic: 1.2.4
/es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, tarball: https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz}
engines: {node: '>= 0.4'}
/es-get-iterator@1.1.3:
@ -19762,7 +19796,7 @@ packages:
dev: true
/function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, tarball: https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz}
/function.prototype.name@1.1.6:
resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==}
@ -19779,7 +19813,6 @@ packages:
/gauge@3.0.2:
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
engines: {node: '>=10'}
deprecated: This package is no longer supported.
dependencies:
aproba: 2.0.0
color-support: 1.1.3
@ -19829,7 +19862,7 @@ packages:
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
/get-intrinsic@1.2.4:
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==, tarball: https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz}
engines: {node: '>= 0.4'}
dependencies:
es-errors: 1.3.0
@ -20132,7 +20165,7 @@ packages:
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
/gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==, tarball: https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz}
dependencies:
get-intrinsic: 1.2.4
@ -20229,16 +20262,16 @@ packages:
engines: {node: '>=8'}
/has-property-descriptors@1.0.2:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==, tarball: https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz}
dependencies:
es-define-property: 1.0.0
/has-proto@1.0.3:
resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==}
resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==, tarball: https://registry.npmmirror.com/has-proto/-/has-proto-1.0.3.tgz}
engines: {node: '>= 0.4'}
/has-symbols@1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==, tarball: https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz}
engines: {node: '>= 0.4'}
/has-tostringtag@1.0.2:
@ -20288,7 +20321,7 @@ packages:
dev: false
/hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, tarball: https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz}
engines: {node: '>= 0.4'}
dependencies:
function-bind: 1.1.2
@ -20688,7 +20721,7 @@ packages:
optional: true
/inflation@2.1.0:
resolution: {integrity: sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==}
resolution: {integrity: sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==, tarball: https://registry.npmmirror.com/inflation/-/inflation-2.1.0.tgz}
engines: {node: '>= 0.8.0'}
/inflection@1.13.4:
@ -22672,7 +22705,7 @@ packages:
dev: true
/media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==, tarball: https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz}
engines: {node: '>= 0.6'}
/mem@8.1.1:
@ -23372,6 +23405,10 @@ packages:
resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==}
dev: false
/node-addon-api@5.1.0:
resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==}
dev: false
/node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
@ -23609,7 +23646,6 @@ packages:
/npmlog@5.0.1:
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
deprecated: This package is no longer supported.
dependencies:
are-we-there-yet: 2.0.0
console-control-strings: 1.1.0
@ -23658,14 +23694,14 @@ packages:
engines: {node: '>= 6'}
/object-inspect@1.12.3:
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==, tarball: https://registry.npmmirror.com/object-inspect/-/object-inspect-1.12.3.tgz}
/object-inspect@1.13.1:
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
dev: false
/object-inspect@1.13.2:
resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==}
resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==, tarball: https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.2.tgz}
engines: {node: '>= 0.4'}
/object-is@1.1.5:
@ -25427,7 +25463,6 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.3.1
dev: true
/qrcode@1.5.3:
resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==}
@ -27564,7 +27599,7 @@ packages:
engines: {node: '>=10'}
/safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, tarball: https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz}
/sanitize-html@2.10.0:
resolution: {integrity: sha512-JqdovUd81dG4k87vZt6uA6YhDfWkUGruUu/aPmXLxXi45gZExnt9Bnw/qeQU8oGf82vPyaE0vO4aH0PbobB9JQ==}
@ -27831,7 +27866,7 @@ packages:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
/set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==, tarball: https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz}
engines: {node: '>= 0.4'}
dependencies:
define-data-property: 1.1.4
@ -27865,7 +27900,7 @@ packages:
resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==}
/setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, tarball: https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz}
/sha.js@2.4.11:
resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
@ -27914,7 +27949,7 @@ packages:
dev: false
/side-channel@1.0.6:
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==, tarball: https://registry.npmmirror.com/side-channel/-/side-channel-1.0.6.tgz}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.7
@ -29210,7 +29245,7 @@ packages:
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
/toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==, tarball: https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz}
engines: {node: '>=0.6'}
/toposort-class@1.0.1:
@ -29601,7 +29636,7 @@ packages:
dev: false
/type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==, tarball: https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz}
engines: {node: '>= 0.6'}
dependencies:
media-typer: 0.3.0