实现邮件验证发送
This commit is contained in:
@@ -20,8 +20,11 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@alicloud/credentials": "^2.4.3",
|
||||||
|
"@alicloud/dm20151123": "1.2.6",
|
||||||
"@alicloud/dysmsapi20170525": "4.1.0",
|
"@alicloud/dysmsapi20170525": "4.1.0",
|
||||||
"@alicloud/openapi-client": "^0.4.14",
|
"@alicloud/openapi-client": "^0.4.14",
|
||||||
|
"@alicloud/tea-util": "^1.4.10",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
|
|||||||
19
tone-page-server/pnpm-lock.yaml
generated
19
tone-page-server/pnpm-lock.yaml
generated
@@ -8,12 +8,21 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@alicloud/credentials':
|
||||||
|
specifier: ^2.4.3
|
||||||
|
version: 2.4.3
|
||||||
|
'@alicloud/dm20151123':
|
||||||
|
specifier: 1.2.6
|
||||||
|
version: 1.2.6
|
||||||
'@alicloud/dysmsapi20170525':
|
'@alicloud/dysmsapi20170525':
|
||||||
specifier: 4.1.0
|
specifier: 4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
'@alicloud/openapi-client':
|
'@alicloud/openapi-client':
|
||||||
specifier: ^0.4.14
|
specifier: ^0.4.14
|
||||||
version: 0.4.14
|
version: 0.4.14
|
||||||
|
'@alicloud/tea-util':
|
||||||
|
specifier: ^1.4.10
|
||||||
|
version: 1.4.10
|
||||||
'@nestjs/common':
|
'@nestjs/common':
|
||||||
specifier: ^10.0.0
|
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)
|
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':
|
'@alicloud/darabonba-string@1.0.3':
|
||||||
resolution: {integrity: sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA==}
|
resolution: {integrity: sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA==}
|
||||||
|
|
||||||
|
'@alicloud/dm20151123@1.2.6':
|
||||||
|
resolution: {integrity: sha512-6pYgy0D5zmUoxfRYwj0ysX4WPw8IfGimaw3ORFj6hF6lTxWpJ3tteOD72i8rw764eZ78TRc4UyET3U9qCaBeaA==}
|
||||||
|
|
||||||
'@alicloud/dysmsapi20170525@4.1.0':
|
'@alicloud/dysmsapi20170525@4.1.0':
|
||||||
resolution: {integrity: sha512-oUmRp6DTI6gGNbrSQK4lW7EouHIB4C0DCbSEA121NvxHC9XKe4cqiPP2VDqgDQiIK43oiFaHKY3rj+IteOWekA==}
|
resolution: {integrity: sha512-oUmRp6DTI6gGNbrSQK4lW7EouHIB4C0DCbSEA121NvxHC9XKe4cqiPP2VDqgDQiIK43oiFaHKY3rj+IteOWekA==}
|
||||||
|
|
||||||
@@ -3434,6 +3446,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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':
|
'@alicloud/dysmsapi20170525@4.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alicloud/openapi-core': 1.0.4
|
'@alicloud/openapi-core': 1.0.4
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { JwtStrategy } from './strategies/jwt.strategy';
|
|||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { VerificationModule } from 'src/verification/verification.module';
|
import { VerificationModule } from 'src/verification/verification.module';
|
||||||
import { OptionalAuthGuard } from './strategies/OptionalAuthGuard';
|
import { OptionalAuthGuard } from './strategies/OptionalAuthGuard';
|
||||||
|
import { NotificationModule } from 'src/notification/notification.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
@@ -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()
|
@Injectable()
|
||||||
export class NotificationService {
|
export class NotificationService {
|
||||||
sendEmail(email: string, subject: string, content: string) {
|
|
||||||
throw new Error(
|
private dm: Dm20151123;
|
||||||
`Email sending is not implemented yet. Email: ${email}, Subject: ${subject}, Content: ${content}`,
|
|
||||||
);
|
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 `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>特恩的日志 - 登录验证码</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; }
|
||||||
|
.container { max-width: 600px; margin: 0px auto; padding: 20px; }
|
||||||
|
.content { padding: 20px 0; }
|
||||||
|
.code-box {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
color: #777;
|
||||||
|
font-size: 12px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="content">
|
||||||
|
<p>您好!您正在尝试登录【特恩的日志】控制台,验证码如下:</p>
|
||||||
|
|
||||||
|
<div class="code-box">
|
||||||
|
<span id="verificationCode">${option.code}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>请注意:</p>
|
||||||
|
<ul>
|
||||||
|
<li>此验证码 <strong>10分钟内</strong> 有效</li>
|
||||||
|
<li>请勿向任何人透露此验证码</li>
|
||||||
|
<li>如非本人操作,请忽略本邮件</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>© 2025 TONE(个人) 版权所有</p>
|
||||||
|
<a href="https://beian.miit.gov.cn/">网站备案号:渝ICP备2023009516号-1</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
} 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('邮件发送失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
import { NotificationService } from 'src/notification/notification.service';
|
import { NotificationService } from 'src/notification/notification.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VerificationService {
|
export class VerificationService {
|
||||||
private readonly logger = new Logger(VerificationService.name);
|
private readonly logger = new Logger(VerificationService.name);
|
||||||
|
|
||||||
constructor(private readonly notificationService: NotificationService) {}
|
constructor(private readonly notificationService: NotificationService) { }
|
||||||
|
|
||||||
private pool: Map<
|
private pool: Map<
|
||||||
string,
|
string,
|
||||||
@@ -18,6 +18,9 @@ export class VerificationService {
|
|||||||
}
|
}
|
||||||
> = new Map();
|
> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated 该方法暂时弃用,因为没有申请到签名
|
||||||
|
*/
|
||||||
async sendPhoneCode(phone: string, type: 'login') {
|
async sendPhoneCode(phone: string, type: 'login') {
|
||||||
const key = `phone:${phone}:${type}`;
|
const key = `phone:${phone}:${type}`;
|
||||||
// 检测是否在冷却时间内
|
// 检测是否在冷却时间内
|
||||||
@@ -31,25 +34,44 @@ export class VerificationService {
|
|||||||
// await this.notificationService.sendSMS(phone, type, code);
|
// await this.notificationService.sendSMS(phone, type, code);
|
||||||
// 存储验证码
|
// 存储验证码
|
||||||
this.saveCode(key, code);
|
this.saveCode(key, code);
|
||||||
|
throw new Error('不允许的登陆方式');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendEmailCode(email: string, type: 'login') {
|
async sendEmailCode(email: string, type: 'login') {
|
||||||
const key = `email:${email}:${type}`;
|
const key = `email:${email}:${type}`;
|
||||||
// 检测是否在冷却时间内
|
// 检测是否在冷却时间内
|
||||||
// TODO
|
if (this.isInCooldownPeriod(key)) {
|
||||||
|
throw new BadRequestException('发送过于频繁,请稍后再试');
|
||||||
|
}
|
||||||
|
|
||||||
// 生成验证码
|
// 生成验证码
|
||||||
const code = this.generateCode();
|
const code = this.generateCode();
|
||||||
this.logger.log(`Email[${email}] code: ${code}`);
|
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);
|
this.saveCode(key, code);
|
||||||
return true;
|
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) {
|
private saveCode(key: string, code: string) {
|
||||||
this.pool.set(key, {
|
this.pool.set(key, {
|
||||||
code: code,
|
code: code,
|
||||||
|
|||||||
Reference in New Issue
Block a user