1.发送邮件时修改模版

2.重置成功时清除登陆错误次数
pull/470/head
nicheng_he 2025-07-24 11:21:44 +08:00
parent 903fe9aa9d
commit 5f98bf24b3
7 changed files with 102 additions and 22 deletions

View File

@ -21,7 +21,13 @@
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item has-feedback name="validateCode" label="邮件验证码"> <a-form-item has-feedback name="validateCode" label="邮件验证码">
<email-code v-model:value="formState.validateCode" :img-code="formState.imgCode" :email="formState.input" :random-str="formState.randomStr" /> <email-code
v-model:value="formState.validateCode"
:img-code="formState.imgCode"
:email="formState.input"
:random-str="formState.randomStr"
verification-type="forgotPassword"
/>
</a-form-item> </a-form-item>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="mobile" tab="手机号找回"> <a-tab-pane key="mobile" tab="手机号找回">
@ -33,7 +39,14 @@
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item name="validateCode" label="手机验证码"> <a-form-item name="validateCode" label="手机验证码">
<sms-code v-model:value="formState.validateCode" :img-code="formState.imgCode" :mobile="formState.input" :phone-code="formState.phoneCode" :random-str="formState.randomStr" /> <sms-code
v-model:value="formState.validateCode"
:img-code="formState.imgCode"
:mobile="formState.input"
:phone-code="formState.phoneCode"
:random-str="formState.randomStr"
verification-type="forgotPassword"
/>
</a-form-item> </a-form-item>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
@ -60,7 +73,7 @@
<a-button type="primary" size="large" html-type="submit" class="submit-button"> 找回密码</a-button> <a-button type="primary" size="large" html-type="submit" class="submit-button"> 找回密码</a-button>
<div class="mt-2"> <div class="mt-2">
<a href="https://certd.docmirror.cn/guide/use/forgotpasswd/" target="_blank"> 管理员无绑定邮箱或MFA丢失找回 </a> <a href="https://certd.docmirror.cn/guide/use/forgotpasswd/" target="_blank"> 管理员无绑定通信方式或MFA丢失找回 </a>
</div> </div>
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -83,7 +96,10 @@ const rules = {
input: [{ required: true }], input: [{ required: true }],
validateCode: [{ required: true }], validateCode: [{ required: true }],
imgCode: [{ required: true }, { min: 4, max: 4, message: "请输入4位图片验证码" }], imgCode: [{ required: true }, { min: 4, max: 4, message: "请输入4位图片验证码" }],
password: [{ required: true, trigger: "change", message: "请输入密码" }], password: [
{ required: true, trigger: "change", message: "请输入密码" },
{ len: 6, message: "至少输入6位密码" },
],
confirmPassword: [ confirmPassword: [
{ required: true, trigger: "change", message: "请确认密码" }, { required: true, trigger: "change", message: "请确认密码" },
{ {

View File

@ -23,6 +23,7 @@ const props = defineProps<{
phoneCode?: string; phoneCode?: string;
imgCode?: string; imgCode?: string;
randomStr?: string; randomStr?: string;
verificationType?: string;
}>(); }>();
const emit = defineEmits(["update:value", "change"]); const emit = defineEmits(["update:value", "change"]);
@ -58,6 +59,7 @@ async function sendSmsCode() {
mobile: props.mobile, mobile: props.mobile,
imgCode: props.imgCode, imgCode: props.imgCode,
randomStr: props.randomStr, randomStr: props.randomStr,
verificationType: props.verificationType,
}); });
} finally { } finally {
loading.value = false; loading.value = false;

View File

@ -22,6 +22,7 @@ const props = defineProps<{
email?: string; email?: string;
imgCode?: string; imgCode?: string;
randomStr?: string; randomStr?: string;
verificationType?: string;
}>(); }>();
const emit = defineEmits(["update:value", "change"]); const emit = defineEmits(["update:value", "change"]);
@ -53,6 +54,7 @@ async function sendSmsCode() {
email: props.email, email: props.email,
imgCode: props.imgCode, imgCode: props.imgCode,
randomStr: props.randomStr, randomStr: props.randomStr,
verificationType: props.verificationType,
}); });
} finally { } finally {
loading.value = false; loading.value = false;

View File

@ -27,6 +27,9 @@ export class EmailCodeReq {
@Rule(RuleType.string().required().max(4)) @Rule(RuleType.string().required().max(4))
imgCode: string; imgCode: string;
@Rule(RuleType.string())
verificationType: string;
} }
/** /**
@ -55,8 +58,20 @@ export class BasicController extends BaseController {
@Body(ALL) @Body(ALL)
body: EmailCodeReq body: EmailCodeReq
) { ) {
const opts = {
verificationType: body.verificationType,
title: undefined,
content: undefined,
duration: undefined,
};
if(body?.verificationType === 'forgotPassword') {
opts.title = '找回密码';
opts.content = '验证码:${code}。您正在找回密码,请输入验证码并完成操作。如非本人操作请忽略';
opts.duration = 3;
}
await this.codeService.checkCaptcha(body.randomStr, body.imgCode); await this.codeService.checkCaptcha(body.randomStr, body.imgCode);
await this.codeService.sendEmailCode(body.email, body.randomStr); await this.codeService.sendEmailCode(body.email, body.randomStr, opts);
// 设置缓存内容 // 设置缓存内容
return this.ok(null); return this.ok(null);
} }

View File

@ -2,12 +2,15 @@ import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { BaseController, CommonException, Constants, SysSettingsService } from "@certd/lib-server"; import { BaseController, CommonException, Constants, SysSettingsService } from "@certd/lib-server";
import { CodeService } from '../../../modules/basic/service/code-service.js'; import { CodeService } from '../../../modules/basic/service/code-service.js';
import { UserService } from '../../../modules/sys/authority/service/user-service.js'; import { UserService } from '../../../modules/sys/authority/service/user-service.js';
import { LoginService } from "../../../modules/login/service/login-service.js";
/** /**
*/ */
@Provide() @Provide()
@Controller('/api') @Controller('/api')
export class LoginController extends BaseController { export class LoginController extends BaseController {
@Inject()
loginService: LoginService;
@Inject() @Inject()
userService: UserService; userService: UserService;
@Inject() @Inject()
@ -23,6 +26,7 @@ export class LoginController extends BaseController {
) { ) {
if(body.type === 'email') { if(body.type === 'email') {
this.codeService.checkEmailCode({ this.codeService.checkEmailCode({
verificationType: 'forgotPassword',
email: body.input, email: body.input,
randomStr: body.randomStr, randomStr: body.randomStr,
validateCode: body.validateCode, validateCode: body.validateCode,
@ -30,6 +34,7 @@ export class LoginController extends BaseController {
}); });
} else if(body.type === 'mobile') { } else if(body.type === 'mobile') {
await this.codeService.checkSmsCode({ await this.codeService.checkSmsCode({
verificationType: 'forgotPassword',
mobile: body.input, mobile: body.input,
randomStr: body.randomStr, randomStr: body.randomStr,
phoneCode: body.phoneCode, phoneCode: body.phoneCode,
@ -39,7 +44,8 @@ export class LoginController extends BaseController {
} else { } else {
throw new CommonException('暂不支持的找回类型,请联系管理员找回'); throw new CommonException('暂不支持的找回类型,请联系管理员找回');
} }
await this.userService.forgotPassword(body); const username = await this.userService.forgotPassword(body);
username && this.loginService.clearCacheOnSuccess(username)
return this.ok(); return this.ok();
} }
} }

View File

@ -57,7 +57,15 @@ export class CodeService {
} }
/** /**
*/ */
async sendSmsCode(phoneCode = '86', mobile: string, randomStr: string) { async sendSmsCode(
phoneCode = '86',
mobile: string,
randomStr: string,
opts?: {
duration?: number,
verificationType?: string
},
) {
if (!mobile) { if (!mobile) {
throw new Error('手机号不能为空'); throw new Error('手机号不能为空');
} }
@ -65,6 +73,8 @@ export class CodeService {
throw new Error('randomStr不能为空'); throw new Error('randomStr不能为空');
} }
const duration = Math.max(Math.floor(Math.min(opts?.duration || 5, 15)), 1);
const sysSettings = await this.sysSettingsService.getPrivateSettings(); const sysSettings = await this.sysSettingsService.getPrivateSettings();
if (!sysSettings.sms?.config?.accessId) { if (!sysSettings.sms?.config?.accessId) {
throw new Error('当前站点还未配置短信'); throw new Error('当前站点还未配置短信');
@ -84,16 +94,29 @@ export class CodeService {
phoneCode, phoneCode,
}); });
const key = this.buildSmsCodeKey(phoneCode, mobile, randomStr); const key = this.buildSmsCodeKey(phoneCode, mobile, randomStr, opts?.verificationType);
cache.set(key, smsCode, { cache.set(key, smsCode, {
ttl: 5 * 60 * 1000, //5分钟 ttl: duration * 60 * 1000, //5分钟
}); });
return smsCode; return smsCode;
} }
/** /**
*
* @param email
* @param randomStr
* @param opts title content duration verificationType
*/ */
async sendEmailCode(email: string, randomStr: string) { async sendEmailCode(
email: string,
randomStr: string,
opts?: {
title?: string,
content?: string,
duration?: number,
verificationType?: string
},
) {
if (!email) { if (!email) {
throw new Error('Email不能为空'); throw new Error('Email不能为空');
} }
@ -110,15 +133,20 @@ export class CodeService {
} }
const code = randomNumber(4); const code = randomNumber(4);
const duration = Math.max(Math.floor(Math.min(opts?.duration || 5, 15)), 1);
const title = `${siteTitle}${!!opts?.title ? opts.title : '验证码'}`;
const content = !!opts.content ? this.compile(opts.content)({code, duration}) : `您的验证码是${code},请勿泄露`;
await this.emailService.send({ await this.emailService.send({
subject: `${siteTitle}】验证码`, subject: title,
content: `您的验证码是${code},请勿泄露`, content: content,
receivers: [email], receivers: [email],
}); });
const key = this.buildEmailCodeKey(email, randomStr); const key = this.buildEmailCodeKey(email, randomStr, opts?.verificationType);
cache.set(key, code, { cache.set(key, code, {
ttl: 5 * 60 * 1000, //5分钟 ttl: duration * 60 * 1000, //5分钟
}); });
return code; return code;
} }
@ -126,20 +154,20 @@ export class CodeService {
/** /**
* checkSms * checkSms
*/ */
async checkSmsCode(opts: { mobile: string; phoneCode: string; smsCode: string; randomStr: string; throwError: boolean }) { async checkSmsCode(opts: { mobile: string; phoneCode: string; smsCode: string; randomStr: string; verificationType?: string; throwError: boolean }) {
const key = this.buildSmsCodeKey(opts.phoneCode, opts.mobile, opts.randomStr); const key = this.buildSmsCodeKey(opts.phoneCode, opts.mobile, opts.randomStr, opts.verificationType);
if (isDev()) { if (isDev()) {
return true; return true;
} }
return this.checkValidateCode(key, opts.smsCode, opts.throwError); return this.checkValidateCode(key, opts.smsCode, opts.throwError);
} }
buildSmsCodeKey(phoneCode: string, mobile: string, randomStr: string) { buildSmsCodeKey(phoneCode: string, mobile: string, randomStr: string, verificationType?: string) {
return `sms:${phoneCode}${mobile}:${randomStr}`; return ['sms', verificationType, phoneCode, mobile, randomStr].filter(item => !!item).join(':');
} }
buildEmailCodeKey(email: string, randomStr: string) { buildEmailCodeKey(email: string, randomStr: string, verificationType?: string) {
return `email:${email}:${randomStr}`; return ['email', verificationType, email, randomStr].filter(item => !!item).join(':');
} }
checkValidateCode(key: string, userCode: string, throwError = true) { checkValidateCode(key: string, userCode: string, throwError = true) {
//验证图片验证码 //验证图片验证码
@ -154,8 +182,18 @@ export class CodeService {
return true; return true;
} }
checkEmailCode(opts: { randomStr: string; validateCode: string; email: string; throwError: boolean }) { checkEmailCode(opts: { randomStr: string; validateCode: string; email: string; verificationType?: string; throwError: boolean }) {
const key = this.buildEmailCodeKey(opts.email, opts.randomStr); const key = this.buildEmailCodeKey(opts.email, opts.randomStr, opts.verificationType);
return this.checkValidateCode(key, opts.validateCode, opts.throwError); return this.checkValidateCode(key, opts.validateCode, opts.throwError);
} }
compile(templateString: string) {
return new Function(
"data",
` with(data || {}) {
return \`${templateString}\`;
}
`
);
}
} }

View File

@ -250,6 +250,7 @@ export class UserService extends BaseService<UserEntity> {
// return; // return;
} }
await this.resetPassword(user.id, data.password) await this.resetPassword(user.id, data.password)
return user.username;
} }
async changePassword(userId: any, form: any) { async changePassword(userId: any, form: any) {