From 9261719589fbfa521c79bcd8d795228026efd11d Mon Sep 17 00:00:00 2001 From: luliangqiang <2650321653@qq.com> Date: Tue, 30 Jul 2024 11:08:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97?= =?UTF-8?q?=E5=8F=B7=E7=99=BB=E5=BD=95=E6=8F=92=E4=BB=B6-=E6=9C=AA?= =?UTF-8?q?=E9=87=8D=E5=AE=9A=E5=90=91=20(#1405)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://git.daoyoucloud.com/daoyoucloud/tachybase/pulls/1405 Reviewed-by: sealday Co-authored-by: luliangqiang <2650321653@qq.com> Co-committed-by: luliangqiang <2650321653@qq.com> --- .../package.json | 11 +- .../src/client/components.tsx | 139 +++++++ .../src/client/index.tsx | 20 +- .../src/client/locale.tsx | 7 + .../src/constants.ts | 2 + .../src/server/actions/work.ts | 39 ++ .../src/server/plugin.ts | 345 +++++++++++++++--- pnpm-lock.yaml | 89 +++-- 8 files changed, 562 insertions(+), 90 deletions(-) create mode 100644 packages/plugins/@tachybase/plugin-wechat-official-account/src/client/components.tsx create mode 100644 packages/plugins/@tachybase/plugin-wechat-official-account/src/client/locale.tsx create mode 100644 packages/plugins/@tachybase/plugin-wechat-official-account/src/constants.ts create mode 100644 packages/plugins/@tachybase/plugin-wechat-official-account/src/server/actions/work.ts diff --git a/packages/plugins/@tachybase/plugin-wechat-official-account/package.json b/packages/plugins/@tachybase/plugin-wechat-official-account/package.json index 00a75d336..0563d4395 100644 --- a/packages/plugins/@tachybase/plugin-wechat-official-account/package.json +++ b/packages/plugins/@tachybase/plugin-wechat-official-account/package.json @@ -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" }, diff --git a/packages/plugins/@tachybase/plugin-wechat-official-account/src/client/components.tsx b/packages/plugins/@tachybase/plugin-wechat-official-account/src/client/components.tsx new file mode 100644 index 000000000..ec735dab4 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-wechat-official-account/src/client/components.tsx @@ -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 ( +
+ +
+ + {t('Redirect URL')}: + + onCopy(redirectUrl)} />} /> +
+
+ ); +}; + +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 ( + <> + + setQrCodeVisible(false)} + > + {loading ? : QR Code} + + + ); +}; diff --git a/packages/plugins/@tachybase/plugin-wechat-official-account/src/client/index.tsx b/packages/plugins/@tachybase/plugin-wechat-official-account/src/client/index.tsx index 3d701fd59..09108a1e5 100644 --- a/packages/plugins/@tachybase/plugin-wechat-official-account/src/client/index.tsx +++ b/packages/plugins/@tachybase/plugin-wechat-official-account/src/client/index.tsx @@ -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; diff --git a/packages/plugins/@tachybase/plugin-wechat-official-account/src/client/locale.tsx b/packages/plugins/@tachybase/plugin-wechat-official-account/src/client/locale.tsx new file mode 100644 index 000000000..e30c0c782 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-wechat-official-account/src/client/locale.tsx @@ -0,0 +1,7 @@ +import { useTranslation as useT } from '@tachybase/client'; + +import { namespace } from '../constants'; + +export function useTranslation() { + return useT([namespace, 'client'], { nsMode: 'fallback' }); +} diff --git a/packages/plugins/@tachybase/plugin-wechat-official-account/src/constants.ts b/packages/plugins/@tachybase/plugin-wechat-official-account/src/constants.ts new file mode 100644 index 000000000..5bdeb8283 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-wechat-official-account/src/constants.ts @@ -0,0 +1,2 @@ +export const authType = '微信公众号'; +export const namespace = '微信公众号'; diff --git a/packages/plugins/@tachybase/plugin-wechat-official-account/src/server/actions/work.ts b/packages/plugins/@tachybase/plugin-wechat-official-account/src/server/actions/work.ts new file mode 100644 index 000000000..a7241b2c5 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-wechat-official-account/src/server/actions/work.ts @@ -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(); +}; diff --git a/packages/plugins/@tachybase/plugin-wechat-official-account/src/server/plugin.ts b/packages/plugins/@tachybase/plugin-wechat-official-account/src/server/plugin.ts index 383a7dd3d..b02c5ee98 100644 --- a/packages/plugins/@tachybase/plugin-wechat-official-account/src/server/plugin.ts +++ b/packages/plugins/@tachybase/plugin-wechat-official-account/src/server/plugin.ts @@ -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 { + // 从数据库查询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 { + 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 = ` + + + + ${Math.floor(Date.now() / 1000)} + + + + `; + } 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); + } + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9effdc3e2..eed7aa34c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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