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:
parent
7e02b5a71c
commit
9261719589
@ -1,12 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "@tachybase/plugin-wechat-official-account",
|
"name": "@tachybase/plugin-wechat-official-account",
|
||||||
"version": "0.21.79",
|
"version": "0.21.73",
|
||||||
|
"keywords": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
"main": "dist/server/index.js",
|
"main": "dist/server/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.3.7",
|
||||||
"@types/xml2js": "^0.4.14",
|
"@types/xml2js": "^0.4.14",
|
||||||
"antd": "^5.18.3",
|
"antd": "^5.18.3",
|
||||||
|
"axios": "^1.7.2",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
"koa-bodyparser": "^4.3.0",
|
"koa-bodyparser": "^4.3.0",
|
||||||
|
"qrcode.react": "^3.1.0",
|
||||||
"react-router-dom": "^6.11.2",
|
"react-router-dom": "^6.11.2",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,17 +1,13 @@
|
|||||||
import React from 'react';
|
import { Plugin } from '@tachybase/client';
|
||||||
import { Plugin, useRequest } 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 {
|
export class PluginWeCahtOfficialAccount extends Plugin {
|
||||||
async afterAdd() {
|
async load() {
|
||||||
// await this.app.pm.add()
|
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;
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
import { useTranslation as useT } from '@tachybase/client';
|
||||||
|
|
||||||
|
import { namespace } from '../constants';
|
||||||
|
|
||||||
|
export function useTranslation() {
|
||||||
|
return useT([namespace, 'client'], { nsMode: 'fallback' });
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
export const authType = '微信公众号';
|
||||||
|
export const namespace = '微信公众号';
|
@ -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();
|
||||||
|
};
|
@ -1,22 +1,78 @@
|
|||||||
|
// --10、解决数据库注册插入问题
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import { BasicAuth } from '@tachybase/plugin-auth';
|
||||||
import { Plugin } from '@tachybase/server';
|
import { Plugin } from '@tachybase/server';
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import xml2js from 'xml2js';
|
import xml2js from 'xml2js';
|
||||||
|
|
||||||
|
import { authType } from '../constants';
|
||||||
|
|
||||||
export class PluginReplacePageServer extends Plugin {
|
export class PluginReplacePageServer extends Plugin {
|
||||||
// 从数据库查询cofig表
|
// 从数据库查询config表
|
||||||
async config(): Promise<any> {
|
async config() {
|
||||||
const repo = this.app.db.getRepository('config');
|
const repo = this.app.db.getRepository('config');
|
||||||
const config = (await repo.findById(1)) as any;
|
const config = await repo.findById(1);
|
||||||
return config.get(); // get()返回具体的data
|
return config.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从数据库查询message表
|
// 从数据库查询message表
|
||||||
async message(): Promise<any[]> {
|
async message() {
|
||||||
const repo = this.app.db.getRepository('message');
|
const repo = this.app.db.getRepository('message');
|
||||||
const messages = (await repo.find()) as any;
|
const messages = await repo.find();
|
||||||
return messages; // 返回数组Type
|
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平台的请求签名
|
// 验证Wechat平台的请求签名
|
||||||
@ -35,8 +91,12 @@ export class PluginReplacePageServer extends Plugin {
|
|||||||
const { appid, appsecret } = await this.config(); // 使用config方法 查询config表
|
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}`;
|
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 {
|
try {
|
||||||
const response = await axios.get(url);
|
const response = await axios.get(url);
|
||||||
|
console.log('Access token response:', response.data);
|
||||||
return response.data.access_token;
|
return response.data.access_token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting access token:', 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() {
|
async createCustomMenu() {
|
||||||
const accessToken = await this.getAccessToken(); // 获取Wechat Token
|
const accessToken = await this.getAccessToken(); // 获取Wechat Token
|
||||||
|
const menu = await this.menu(); // 获取菜单数据
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
console.error('Failed to get access token');
|
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 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请求,创建菜单
|
// 若通过accessToken验证,则发送Wechat请求,创建菜单
|
||||||
try {
|
try {
|
||||||
@ -93,7 +148,7 @@ export class PluginReplacePageServer extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wechat从用户这里获取签名, 若通过则允许后续交互
|
// Wechat从用户这里获取签名,若通过则允许后续交互
|
||||||
async get(ctx) {
|
async get(ctx) {
|
||||||
const { query } = ctx.request;
|
const { query } = ctx.request;
|
||||||
if (await this.checkSignature(query)) {
|
if (await this.checkSignature(query)) {
|
||||||
@ -114,10 +169,9 @@ export class PluginReplacePageServer extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messages = await this.message(); // 调用当前对象的 message方法
|
const messages = await this.message(); // 调用当前对象的 message方法
|
||||||
// const keywordsList = keywords.map(keyword => keyword.keywords).join(`${keywords}`);
|
const messagesList = messages.map((message) => `${message.id}、 ${message.key}`).join('\n');
|
||||||
const messagesList = messages.map((message, index) => `${index + 1}、 ${message.key}`).join('\n');
|
|
||||||
|
|
||||||
let replyContent = `您好,欢迎关注道有云网络科技有限公司!您可以输入下面关键字来获取您要了解的信息!\n(输入序号即可):\n${messagesList}`; // 默认回复内容
|
let replyContent = `您好,欢迎关注道有云网络科技有限公司!您可以输入下面关键字来获取您要了解的信息!\n(输入序号或关键字):\n${messagesList}`;
|
||||||
|
|
||||||
// 解析 POST 请求的 XML 格式消息体
|
// 解析 POST 请求的 XML 格式消息体
|
||||||
xml2js.parseString(body, { explicitArray: false }, (err, result) => {
|
xml2js.parseString(body, { explicitArray: false }, (err, result) => {
|
||||||
@ -131,14 +185,28 @@ export class PluginReplacePageServer extends Plugin {
|
|||||||
const msg = result.xml;
|
const msg = result.xml;
|
||||||
let replyMessage; // 处理用户输入后的回复结果
|
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 (valueById) {
|
||||||
if (values?.length > 0) {
|
replyContent = valueById.value;
|
||||||
// 问号? 异常处理
|
} else if (valueByKey) {
|
||||||
replyContent = values[0].value;
|
replyContent = valueByKey.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
replyMessage = `
|
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() {
|
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({
|
this.app.resourcer.define({
|
||||||
name: 'wechat',
|
name: 'wechat',
|
||||||
actions: {
|
actions: {
|
||||||
|
// 测试接口
|
||||||
test: async (ctx) => {
|
test: async (ctx) => {
|
||||||
ctx.withoutDataWrapping = true;
|
ctx.withoutDataWrapping = true;
|
||||||
ctx.body = 'Hello World!';
|
ctx.body = 'Hello World!';
|
||||||
console.log('Hello World!');
|
console.log('Hello World!');
|
||||||
},
|
},
|
||||||
|
// 创建菜单接口
|
||||||
createMenu: async (ctx) => {
|
createMenu: async (ctx) => {
|
||||||
await this.createCustomMenu();
|
await this.createCustomMenu();
|
||||||
console.log('Custom menu created successfully.');
|
console.log('Custom menu created successfully.');
|
||||||
ctx.body = 'success';
|
ctx.body = 'success';
|
||||||
},
|
},
|
||||||
|
// Wechat 消息处理接口/服务器URL配置接口
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
ctx.withoutDataWrapping = true;
|
ctx.withoutDataWrapping = true;
|
||||||
if (ctx.request.method.toLowerCase() === 'get') {
|
if (ctx.request.method.toLowerCase() === 'get') {
|
||||||
//toLowerCase()转换字符串为小写
|
|
||||||
await this.get(ctx);
|
await this.get(ctx);
|
||||||
} else {
|
} else {
|
||||||
await this.post(ctx);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4045,6 +4045,9 @@ importers:
|
|||||||
|
|
||||||
packages/plugins/@tachybase/plugin-wechat-official-account:
|
packages/plugins/@tachybase/plugin-wechat-official-account:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@ant-design/icons':
|
||||||
|
specifier: ^5.3.7
|
||||||
|
version: 5.3.7(react-dom@18.3.1)(react@18.3.1)
|
||||||
'@tachybase/client':
|
'@tachybase/client':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../core/client
|
version: link:../../../core/client
|
||||||
@ -4063,12 +4066,27 @@ importers:
|
|||||||
antd:
|
antd:
|
||||||
specifier: 5.19.1
|
specifier: 5.19.1
|
||||||
version: 5.19.1(react-dom@18.3.1)(react@18.3.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:
|
body-parser:
|
||||||
specifier: ^1.20.2
|
specifier: ^1.20.2
|
||||||
version: 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:
|
koa-bodyparser:
|
||||||
specifier: ^4.3.0
|
specifier: ^4.3.0
|
||||||
version: 4.4.1
|
version: 4.4.1
|
||||||
|
qrcode.react:
|
||||||
|
specifier: ^3.1.0
|
||||||
|
version: 3.1.0(react@18.3.1)
|
||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^6.11.2
|
specifier: ^6.11.2
|
||||||
version: 6.25.1(react-dom@18.3.1)(react@18.3.1)
|
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:
|
/are-we-there-yet@2.0.0:
|
||||||
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
|
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
deprecated: This package is no longer supported.
|
|
||||||
dependencies:
|
dependencies:
|
||||||
delegates: 1.0.0
|
delegates: 1.0.0
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
@ -15303,6 +15320,18 @@ packages:
|
|||||||
tweetnacl: 0.14.5
|
tweetnacl: 0.14.5
|
||||||
dev: true
|
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:
|
/bessel@1.0.2:
|
||||||
resolution: {integrity: sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw==}
|
resolution: {integrity: sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
@ -15795,7 +15824,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/call-bind@1.0.7:
|
/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'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
es-define-property: 1.0.0
|
es-define-property: 1.0.0
|
||||||
@ -16214,7 +16243,7 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
/co-body@6.1.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:
|
dependencies:
|
||||||
inflation: 2.1.0
|
inflation: 2.1.0
|
||||||
qs: 6.12.3
|
qs: 6.12.3
|
||||||
@ -16747,7 +16776,7 @@ packages:
|
|||||||
toggle-selection: 1.0.6
|
toggle-selection: 1.0.6
|
||||||
|
|
||||||
/copy-to@2.0.1:
|
/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:
|
/core-js-compat@3.34.0:
|
||||||
resolution: {integrity: sha512-4ZIyeNbW/Cn1wkMMDy+mvrRUxrwFNjKwbhCfQpDd+eLgYipDqp8oGFGtLmhh18EDPKA0g3VUBYOxQGGwvWLVpA==}
|
resolution: {integrity: sha512-4ZIyeNbW/Cn1wkMMDy+mvrRUxrwFNjKwbhCfQpDd+eLgYipDqp8oGFGtLmhh18EDPKA0g3VUBYOxQGGwvWLVpA==}
|
||||||
@ -16987,6 +17016,11 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: false
|
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):
|
/css-blank-pseudo@3.0.3(postcss@8.4.39):
|
||||||
resolution: {integrity: sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==}
|
resolution: {integrity: sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==}
|
||||||
engines: {node: ^12 || ^14 || >=16}
|
engines: {node: ^12 || ^14 || >=16}
|
||||||
@ -17743,7 +17777,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/define-data-property@1.1.4:
|
/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'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
es-define-property: 1.0.0
|
es-define-property: 1.0.0
|
||||||
@ -18356,13 +18390,13 @@ packages:
|
|||||||
which-typed-array: 1.1.15
|
which-typed-array: 1.1.15
|
||||||
|
|
||||||
/es-define-property@1.0.0:
|
/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'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic: 1.2.4
|
get-intrinsic: 1.2.4
|
||||||
|
|
||||||
/es-errors@1.3.0:
|
/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'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
/es-get-iterator@1.1.3:
|
/es-get-iterator@1.1.3:
|
||||||
@ -19762,7 +19796,7 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/function-bind@1.1.2:
|
/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:
|
/function.prototype.name@1.1.6:
|
||||||
resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==}
|
resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==}
|
||||||
@ -19779,7 +19813,6 @@ packages:
|
|||||||
/gauge@3.0.2:
|
/gauge@3.0.2:
|
||||||
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
|
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
deprecated: This package is no longer supported.
|
|
||||||
dependencies:
|
dependencies:
|
||||||
aproba: 2.0.0
|
aproba: 2.0.0
|
||||||
color-support: 1.1.3
|
color-support: 1.1.3
|
||||||
@ -19829,7 +19862,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
|
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
|
||||||
|
|
||||||
/get-intrinsic@1.2.4:
|
/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'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
@ -20132,7 +20165,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
|
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
|
||||||
|
|
||||||
/gopd@1.0.1:
|
/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:
|
dependencies:
|
||||||
get-intrinsic: 1.2.4
|
get-intrinsic: 1.2.4
|
||||||
|
|
||||||
@ -20229,16 +20262,16 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
/has-property-descriptors@1.0.2:
|
/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:
|
dependencies:
|
||||||
es-define-property: 1.0.0
|
es-define-property: 1.0.0
|
||||||
|
|
||||||
/has-proto@1.0.3:
|
/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'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
/has-symbols@1.0.3:
|
/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'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
/has-tostringtag@1.0.2:
|
/has-tostringtag@1.0.2:
|
||||||
@ -20288,7 +20321,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/hasown@2.0.2:
|
/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'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
@ -20688,7 +20721,7 @@ packages:
|
|||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/inflation@2.1.0:
|
/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'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
/inflection@1.13.4:
|
/inflection@1.13.4:
|
||||||
@ -22672,7 +22705,7 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/media-typer@0.3.0:
|
/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'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
/mem@8.1.1:
|
/mem@8.1.1:
|
||||||
@ -23372,6 +23405,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==}
|
resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/node-addon-api@5.1.0:
|
||||||
|
resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/node-domexception@1.0.0:
|
/node-domexception@1.0.0:
|
||||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
engines: {node: '>=10.5.0'}
|
engines: {node: '>=10.5.0'}
|
||||||
@ -23609,7 +23646,6 @@ packages:
|
|||||||
|
|
||||||
/npmlog@5.0.1:
|
/npmlog@5.0.1:
|
||||||
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
|
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
|
||||||
deprecated: This package is no longer supported.
|
|
||||||
dependencies:
|
dependencies:
|
||||||
are-we-there-yet: 2.0.0
|
are-we-there-yet: 2.0.0
|
||||||
console-control-strings: 1.1.0
|
console-control-strings: 1.1.0
|
||||||
@ -23658,14 +23694,14 @@ packages:
|
|||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
/object-inspect@1.12.3:
|
/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:
|
/object-inspect@1.13.1:
|
||||||
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
|
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/object-inspect@1.13.2:
|
/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'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
/object-is@1.1.5:
|
/object-is@1.1.5:
|
||||||
@ -25427,7 +25463,6 @@ packages:
|
|||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/qrcode@1.5.3:
|
/qrcode@1.5.3:
|
||||||
resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==}
|
resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==}
|
||||||
@ -27564,7 +27599,7 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
/safer-buffer@2.1.2:
|
/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:
|
/sanitize-html@2.10.0:
|
||||||
resolution: {integrity: sha512-JqdovUd81dG4k87vZt6uA6YhDfWkUGruUu/aPmXLxXi45gZExnt9Bnw/qeQU8oGf82vPyaE0vO4aH0PbobB9JQ==}
|
resolution: {integrity: sha512-JqdovUd81dG4k87vZt6uA6YhDfWkUGruUu/aPmXLxXi45gZExnt9Bnw/qeQU8oGf82vPyaE0vO4aH0PbobB9JQ==}
|
||||||
@ -27831,7 +27866,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
|
||||||
/set-function-length@1.2.2:
|
/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'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
define-data-property: 1.1.4
|
define-data-property: 1.1.4
|
||||||
@ -27865,7 +27900,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==}
|
resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==}
|
||||||
|
|
||||||
/setprototypeof@1.2.0:
|
/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:
|
/sha.js@2.4.11:
|
||||||
resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
|
resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
|
||||||
@ -27914,7 +27949,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/side-channel@1.0.6:
|
/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'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
@ -29210,7 +29245,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
|
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
|
||||||
|
|
||||||
/toidentifier@1.0.1:
|
/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'}
|
engines: {node: '>=0.6'}
|
||||||
|
|
||||||
/toposort-class@1.0.1:
|
/toposort-class@1.0.1:
|
||||||
@ -29601,7 +29636,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/type-is@1.6.18:
|
/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'}
|
engines: {node: '>= 0.6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
media-typer: 0.3.0
|
media-typer: 0.3.0
|
||||||
|
Loading…
Reference in New Issue
Block a user