From 7adcede1cdfc22713f02b7bfa921ba4a78042a7f Mon Sep 17 00:00:00 2001 From: tone <3341154833@qq.com> Date: Thu, 19 Jun 2025 11:21:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E9=82=AE=E4=BB=B6=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tone-page-server/package.json | 3 + tone-page-server/pnpm-lock.yaml | 19 ++++ tone-page-server/src/auth/auth.module.ts | 1 + .../src/notification/notification.service.ts | 106 +++++++++++++++++- .../src/verification/verification.service.ts | 30 ++++- 5 files changed, 150 insertions(+), 9 deletions(-) diff --git a/tone-page-server/package.json b/tone-page-server/package.json index f5cc35c..0244745 100644 --- a/tone-page-server/package.json +++ b/tone-page-server/package.json @@ -20,8 +20,11 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@alicloud/credentials": "^2.4.3", + "@alicloud/dm20151123": "1.2.6", "@alicloud/dysmsapi20170525": "4.1.0", "@alicloud/openapi-client": "^0.4.14", + "@alicloud/tea-util": "^1.4.10", "@nestjs/common": "^10.0.0", "@nestjs/config": "^4.0.2", "@nestjs/core": "^10.0.0", diff --git a/tone-page-server/pnpm-lock.yaml b/tone-page-server/pnpm-lock.yaml index 24bb9f0..8d754a1 100644 --- a/tone-page-server/pnpm-lock.yaml +++ b/tone-page-server/pnpm-lock.yaml @@ -8,12 +8,21 @@ importers: .: dependencies: + '@alicloud/credentials': + specifier: ^2.4.3 + version: 2.4.3 + '@alicloud/dm20151123': + specifier: 1.2.6 + version: 1.2.6 '@alicloud/dysmsapi20170525': specifier: 4.1.0 version: 4.1.0 '@alicloud/openapi-client': specifier: ^0.4.14 version: 0.4.14 + '@alicloud/tea-util': + specifier: ^1.4.10 + version: 1.4.10 '@nestjs/common': specifier: ^10.0.0 version: 10.4.17(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -168,6 +177,9 @@ packages: '@alicloud/darabonba-string@1.0.3': resolution: {integrity: sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA==} + '@alicloud/dm20151123@1.2.6': + resolution: {integrity: sha512-6pYgy0D5zmUoxfRYwj0ysX4WPw8IfGimaw3ORFj6hF6lTxWpJ3tteOD72i8rw764eZ78TRc4UyET3U9qCaBeaA==} + '@alicloud/dysmsapi20170525@4.1.0': resolution: {integrity: sha512-oUmRp6DTI6gGNbrSQK4lW7EouHIB4C0DCbSEA121NvxHC9XKe4cqiPP2VDqgDQiIK43oiFaHKY3rj+IteOWekA==} @@ -3434,6 +3446,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@alicloud/dm20151123@1.2.6': + dependencies: + '@alicloud/openapi-core': 1.0.4 + '@darabonba/typescript': 1.0.3 + transitivePeerDependencies: + - supports-color + '@alicloud/dysmsapi20170525@4.1.0': dependencies: '@alicloud/openapi-core': 1.0.4 diff --git a/tone-page-server/src/auth/auth.module.ts b/tone-page-server/src/auth/auth.module.ts index 6c13af9..870b2f0 100644 --- a/tone-page-server/src/auth/auth.module.ts +++ b/tone-page-server/src/auth/auth.module.ts @@ -10,6 +10,7 @@ import { JwtStrategy } from './strategies/jwt.strategy'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { VerificationModule } from 'src/verification/verification.module'; import { OptionalAuthGuard } from './strategies/OptionalAuthGuard'; +import { NotificationModule } from 'src/notification/notification.module'; @Module({ imports: [ diff --git a/tone-page-server/src/notification/notification.service.ts b/tone-page-server/src/notification/notification.service.ts index 6461a7b..538a4e7 100644 --- a/tone-page-server/src/notification/notification.service.ts +++ b/tone-page-server/src/notification/notification.service.ts @@ -1,11 +1,107 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; + +import Dm20151123, * as $Dm20151123 from '@alicloud/dm20151123'; +import OpenApi, * as $OpenApi from '@alicloud/openapi-client'; +import Client, * as $dm from "@alicloud/dm20151123"; +import Util, * as $Util from '@alicloud/tea-util'; +import Credential, { Config } from '@alicloud/credentials'; @Injectable() export class NotificationService { - sendEmail(email: string, subject: string, content: string) { - throw new Error( - `Email sending is not implemented yet. Email: ${email}, Subject: ${subject}, Content: ${content}`, - ); + + private dm: Dm20151123; + + constructor() { + const credentialsConfig = new Config({ + type: 'access_key', + accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID, + accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET, + }); + const credential = new Credential(credentialsConfig); + const config = new $OpenApi.Config({ credential }); + config.endpoint = 'dm.aliyuncs.com'; + this.dm = new Dm20151123(config); + } + + private getMailHtmlBody(option: { type: 'login-verify', code: string }) { + if (option.type === 'login-verify') { + return ` + + + + 特恩的日志 - 登录验证码 + + + +
+
+

您好!您正在尝试登录【特恩的日志】控制台,验证码如下:

+ +
+ ${option.code} +
+ +

请注意:

+ +
+ + +
+ + ` + } else { + throw new Error('未配置的模版'); + } + } + + + async sendMail(option: { type: 'login-verify', targetMail: string, code: string; }) { + const runtime = new $Util.RuntimeOptions({}); + + const singleSendMailRequest = new $Dm20151123.SingleSendMailRequest({ + accountName: "security@tonesc.cn", + addressType: 1, + replyToAddress: false, + toAddress: `${option.targetMail}`, + subject: "【特恩的日志】登陆验证码", + htmlBody: this.getMailHtmlBody({ type: 'login-verify', code: option.code }), + textBody: "", + }) + + try { + await this.dm.singleSendMailWithOptions(singleSendMailRequest, runtime); + } catch (error) { + console.error(error); + throw new BadRequestException('邮件发送失败'); + } } /** diff --git a/tone-page-server/src/verification/verification.service.ts b/tone-page-server/src/verification/verification.service.ts index e41ef73..9168f51 100644 --- a/tone-page-server/src/verification/verification.service.ts +++ b/tone-page-server/src/verification/verification.service.ts @@ -1,11 +1,11 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { NotificationService } from 'src/notification/notification.service'; @Injectable() export class VerificationService { private readonly logger = new Logger(VerificationService.name); - constructor(private readonly notificationService: NotificationService) {} + constructor(private readonly notificationService: NotificationService) { } private pool: Map< string, @@ -18,6 +18,9 @@ export class VerificationService { } > = new Map(); + /** + * @deprecated 该方法暂时弃用,因为没有申请到签名 + */ async sendPhoneCode(phone: string, type: 'login') { const key = `phone:${phone}:${type}`; // 检测是否在冷却时间内 @@ -31,25 +34,44 @@ export class VerificationService { // await this.notificationService.sendSMS(phone, type, code); // 存储验证码 this.saveCode(key, code); + throw new Error('不允许的登陆方式'); return true; } async sendEmailCode(email: string, type: 'login') { const key = `email:${email}:${type}`; // 检测是否在冷却时间内 - // TODO + if (this.isInCooldownPeriod(key)) { + throw new BadRequestException('发送过于频繁,请稍后再试'); + } // 生成验证码 const code = this.generateCode(); this.logger.log(`Email[${email}] code: ${code}`); // 发送验证码 - // TODO + await this.notificationService.sendMail({ type: 'login-verify', targetMail: email, code, }).catch(() => { + throw new BadRequestException('发送失败,请稍后再试') + }) // 存储验证码 this.saveCode(key, code); return true; } + private isInCooldownPeriod(key: string) { + const item = this.pool.get(key); + if (!item) { + return false; + } + + // 冷却60秒 + if (Date.now() - item.createdAt > 60 * 1000) { + return false; + } + + return true; + } + private saveCode(key: string, code: string) { this.pool.set(key, { code: code,