diff --git a/packages/core/utils/src/__tests__/parsedValue.test.ts b/packages/core/utils/src/__tests__/parsedValue.test.ts new file mode 100644 index 0000000000..6bdb153b05 --- /dev/null +++ b/packages/core/utils/src/__tests__/parsedValue.test.ts @@ -0,0 +1,42 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { parsedValue } from "../parsedValue"; + +describe('parsedValue', () => { + it('should correctly parse simple templates', () => { + const value = '{{name}} is {{age}} years old'; + const variables = { name: 'Zhang San', age: 18 }; + expect(parsedValue(value, variables)).toBe('Zhang San is 18 years old'); + }); + + it('should correctly parse that the template has just one variable', () => { + const value = '{{name}}'; + const variables = { name: 'Zhang San' }; + expect(parsedValue(value, variables)).toBe('Zhang San'); + }); + + it('should handle nested objects', () => { + const value = "{{user.name}}'s email is {{user.email}}"; + const variables = { user: { name: 'Li Si', email: 'lisi@example.com' } }; + expect(parsedValue(value, variables)).toBe("Li Si's email is lisi@example.com"); + }); + + it('should handle arrays', () => { + const value = '{{fruits.0}} and {{fruits.1}}'; + const variables = { fruits: ['apple', 'banana'] }; + expect(parsedValue(value, variables)).toBe('apple and banana'); + }); + + it('should handle undefined variables', () => { + const value = '{{name}} and {{undefinedVar}}'; + const variables = { name: 'Wang Wu' }; + expect(parsedValue(value, variables)).toBe('Wang Wu and '); + }); +}); diff --git a/packages/core/utils/src/index.ts b/packages/core/utils/src/index.ts index 4b91d6e8f5..64e2f4458d 100644 --- a/packages/core/utils/src/index.ts +++ b/packages/core/utils/src/index.ts @@ -39,4 +39,5 @@ export * from './url'; export * from './i18n'; export * from './wrap-middleware'; export * from './object-to-cli-args'; +export * from './parsedValue'; export { Schema } from '@formily/json-schema'; diff --git a/packages/core/utils/src/parsedValue.ts b/packages/core/utils/src/parsedValue.ts new file mode 100644 index 0000000000..27f71852da --- /dev/null +++ b/packages/core/utils/src/parsedValue.ts @@ -0,0 +1,22 @@ +import { parse } from "./json-templates"; + +function appendArrayColumn(scope, key) { + const paths = key.split('.'); + let data = scope; + for (let p = 0; p < paths.length && data != null; p++) { + const path = paths[p]; + const isIndex = path.match(/^\d+$/); + if (Array.isArray(data) && !isIndex && !data[path]) { + data[path] = data.map((item) => item[path]).flat(); + } + data = data?.[path]; + } +} + +export const parsedValue = (value, variables) => { + const template = parse(value); + template.parameters.forEach(({ key }) => { + appendArrayColumn(variables, key); + }); + return template(variables); +}; diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/basic-auth.ts b/packages/plugins/@nocobase/plugin-auth/src/server/basic-auth.ts index 5a378e9ddd..b41af66799 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/server/basic-auth.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/server/basic-auth.ts @@ -9,9 +9,9 @@ import { AuthConfig, BaseAuth } from '@nocobase/auth'; import { PasswordField } from '@nocobase/database'; -import crypto from 'crypto'; -import { namespace } from '../preset'; import _ from 'lodash'; +import { namespace } from '../preset'; +import { getDateVars, parsedValue } from '@nocobase/utils'; export class BasicAuth extends BaseAuth { constructor(config: AuthConfig) { @@ -33,8 +33,8 @@ export class BasicAuth extends BaseAuth { const filter = email ? { email } : { - $or: [{ username: account }, { email: account }], - }; + $or: [{ username: account }, { email: account }], + }; const user = await this.userRepository.findOne({ filter, }); @@ -139,19 +139,55 @@ export class BasicAuth extends BaseAuth { const { values: { email }, } = ctx.action.params; + if (!email) { ctx.throw(400, ctx.t('Please fill in your email address', { ns: namespace })); } + const user = await this.userRepository.findOne({ where: { email, }, }); + if (!user) { ctx.throw(401, ctx.t('The email is incorrect, please re-enter', { ns: namespace })); } - user.resetToken = crypto.randomBytes(20).toString('hex'); - await user.save(); + + // 通过用户认证的接口获取邮件渠道、主题、内容等 + const { emailChannel, subject, contentType, content, expiresIn } = this.getEmailConfig(); + + // 生成重置密码的 token + const resetToken = await ctx.app.authManager.jwt.sign({ + resetPasswordUserId: user.id, + }, { + expiresIn, // 配置的过期时间 + }); + + // 通过通知管理插件发送邮件 + const notificationManager = ctx.app.getPlugin('notification-manager'); + if (notificationManager) { + const emailer = notificationManager.getChannel(emailChannel); + if (emailer) { + await emailer.send({ + to: email, + subject, + content: parsedValue(content, { + $user: user, + $resetLink: `${ctx.request.protocol}://${ctx.request.host}/reset-password?token=${resetToken}`, + $date: getDateVars(), + $env: ctx.app.environment.getVariables(), + }), + }); + } else { + ctx.throw(400, ctx.t('Email channel not found', { ns: namespace })); + } + } else { + ctx.throw(500, ctx.t('Notification manager plugin not found', { ns: namespace })); + } + + ctx.logger.info(`Password reset email sent to ${email}`); + return user; }