diff --git a/packages/libs/lib-server/src/system/settings/service/models.ts b/packages/libs/lib-server/src/system/settings/service/models.ts index 851c37d9..c31f14fb 100644 --- a/packages/libs/lib-server/src/system/settings/service/models.ts +++ b/packages/libs/lib-server/src/system/settings/service/models.ts @@ -22,6 +22,7 @@ export class SysPublicSettings extends BaseSettings { mobileRegisterEnabled = false; smsLoginEnabled = false; emailRegisterEnabled = false; + selfServicePasswordRetrievalEnabled = false; limitUserPipelineCount = 0; managerOtherUserPipeline = false; diff --git a/packages/ui/certd-client/src/locales/langs/en-US/authentication.ts b/packages/ui/certd-client/src/locales/langs/en-US/authentication.ts index 33b150b3..3109985a 100644 --- a/packages/ui/certd-client/src/locales/langs/en-US/authentication.ts +++ b/packages/ui/certd-client/src/locales/langs/en-US/authentication.ts @@ -57,6 +57,7 @@ export default { passwordPlaceholder: "Please enter your password", mobilePlaceholder: "Please enter your mobile number", loginButton: "Log In", + forgotPassword: "Forgot password?", forgotAdminPassword: "Forgot admin password?", registerLink: "Register", diff --git a/packages/ui/certd-client/src/locales/langs/en-US/certd.ts b/packages/ui/certd-client/src/locales/langs/en-US/certd.ts index 0ceed385..4e734a54 100644 --- a/packages/ui/certd-client/src/locales/langs/en-US/certd.ts +++ b/packages/ui/certd-client/src/locales/langs/en-US/certd.ts @@ -565,6 +565,7 @@ export default { dualStackNetworkHelper: "If IPv6 priority is selected, enable IPv6 in docker-compose.yaml", enableCommonCnameService: "Enable Public CNAME Service", commonCnameHelper: "Allow use of public CNAME service. If disabled and no custom CNAME service is set, CNAME proxy certificate application will not work.", + enableCommonSelfServicePasswordRetrieval: "Enable self-service password recovery", saveButton: "Save", stopSuccess: "Stopped successfully", google: "Google", diff --git a/packages/ui/certd-client/src/locales/langs/zh-CN/authentication.ts b/packages/ui/certd-client/src/locales/langs/zh-CN/authentication.ts index c71bb400..6df63d0b 100644 --- a/packages/ui/certd-client/src/locales/langs/zh-CN/authentication.ts +++ b/packages/ui/certd-client/src/locales/langs/zh-CN/authentication.ts @@ -57,6 +57,7 @@ export default { passwordPlaceholder: "请输入密码", mobilePlaceholder: "请输入手机号", loginButton: "登录", + forgotPassword: "忘记密码?", forgotAdminPassword: "忘记管理员密码?", registerLink: "注册", diff --git a/packages/ui/certd-client/src/locales/langs/zh-CN/certd.ts b/packages/ui/certd-client/src/locales/langs/zh-CN/certd.ts index 3a7feebe..ec961220 100644 --- a/packages/ui/certd-client/src/locales/langs/zh-CN/certd.ts +++ b/packages/ui/certd-client/src/locales/langs/zh-CN/certd.ts @@ -571,6 +571,7 @@ export default { dualStackNetworkHelper: "如果选择IPv6优先,需要在docker-compose.yaml中启用ipv6", enableCommonCnameService: "启用公共CNAME服务", commonCnameHelper: "是否可以使用公共CNAME服务,如果禁用,且没有设置自定义CNAME服务,则无法使用CNAME代理方式申请证书", + enableCommonSelfServicePasswordRetrieval: "启用自助找回密码", saveButton: "保存", stopSuccess: "停止成功", google: "Google", diff --git a/packages/ui/certd-client/src/router/source/outside.ts b/packages/ui/certd-client/src/router/source/outside.ts index b86ddbdb..5c9bdfa4 100644 --- a/packages/ui/certd-client/src/router/source/outside.ts +++ b/packages/ui/certd-client/src/router/source/outside.ts @@ -24,6 +24,14 @@ export const outsideResource = [ path: "/register", component: "/framework/register/index.vue", }, + { + meta: { + title: "找回密码", + }, + name: "forgotPassword", + path: "/forgotPassword", + component: "/framework/forgot-password/index.vue", + }, ], }, ...errorPage, diff --git a/packages/ui/certd-client/src/store/settings/api.basic.ts b/packages/ui/certd-client/src/store/settings/api.basic.ts index 2df0c02b..ceaa1681 100644 --- a/packages/ui/certd-client/src/store/settings/api.basic.ts +++ b/packages/ui/certd-client/src/store/settings/api.basic.ts @@ -36,6 +36,7 @@ export type SysPublicSetting = { emailRegisterEnabled?: boolean; passwordLoginEnabled?: boolean; smsLoginEnabled?: boolean; + selfServicePasswordRetrievalEnabled?: boolean; limitUserPipelineCount?: number; managerOtherUserPipeline?: boolean; diff --git a/packages/ui/certd-client/src/store/user/api.user.ts b/packages/ui/certd-client/src/store/user/api.user.ts index bfd61d41..c37202a5 100644 --- a/packages/ui/certd-client/src/store/user/api.user.ts +++ b/packages/ui/certd-client/src/store/user/api.user.ts @@ -20,6 +20,17 @@ export interface SmsLoginReq { randomStr: string; } +export interface ForgotPasswordReq { + forgotPasswordType: string; + input: string; + randomStr: string; + imgCode: string; + validateCode: string; + + password: string; + confirmPassword: string; +} + export interface UserInfoRes { id: string | number; username: string; @@ -43,6 +54,13 @@ export async function register(user: RegisterReq): Promise { data: user, }); } +export async function forgotPassword(data: ForgotPasswordReq): Promise { + return await request({ + url: "/forgotPassword", + method: "post", + data: data, + }); +} export async function logout() { return await request({ url: "/logout", diff --git a/packages/ui/certd-client/src/store/user/index.ts b/packages/ui/certd-client/src/store/user/index.ts index 9010cebc..cca8e848 100644 --- a/packages/ui/certd-client/src/store/user/index.ts +++ b/packages/ui/certd-client/src/store/user/index.ts @@ -4,7 +4,7 @@ import router from "../../router"; import { LocalStorage } from "/src/utils/util.storage"; // @ts-ignore import * as UserApi from "./api.user"; -import { RegisterReq, SmsLoginReq } from "./api.user"; +import { ForgotPasswordReq, RegisterReq, SmsLoginReq } from "./api.user"; // @ts-ignore import { LoginReq, UserInfoRes } from "/@/store/user/api.user"; import { message, Modal, notification } from "ant-design-vue"; @@ -67,6 +67,13 @@ export const useUserStore = defineStore({ }); await router.replace("/login"); }, + async forgotPassword(params: ForgotPasswordReq): Promise { + await UserApi.forgotPassword(params); + notification.success({ + message: "密码已重置,请登录", + }); + await router.replace("/login"); + }, /** * @description: login */ diff --git a/packages/ui/certd-client/src/views/framework/forgot-password/index.vue b/packages/ui/certd-client/src/views/framework/forgot-password/index.vue new file mode 100644 index 00000000..7923de31 --- /dev/null +++ b/packages/ui/certd-client/src/views/framework/forgot-password/index.vue @@ -0,0 +1,179 @@ + + + + diff --git a/packages/ui/certd-client/src/views/framework/login/image-code.vue b/packages/ui/certd-client/src/views/framework/login/image-code.vue index 9ac1dae8..8b03ce8a 100644 --- a/packages/ui/certd-client/src/views/framework/login/image-code.vue +++ b/packages/ui/certd-client/src/views/framework/login/image-code.vue @@ -11,7 +11,7 @@ diff --git a/packages/ui/certd-client/src/views/framework/login/index.vue b/packages/ui/certd-client/src/views/framework/login/index.vue index bad1d72f..1f6a0a71 100644 --- a/packages/ui/certd-client/src/views/framework/login/index.vue +++ b/packages/ui/certd-client/src/views/framework/login/index.vue @@ -47,10 +47,10 @@ {{ t("authentication.loginButton") }} -
- - {{ t("authentication.forgotAdminPassword") }} - +
+ + {{ t("authentication.forgotPassword") }} +
diff --git a/packages/ui/certd-client/src/views/framework/login/sms-code.vue b/packages/ui/certd-client/src/views/framework/login/sms-code.vue index b9f6e1ac..efbf6ab3 100644 --- a/packages/ui/certd-client/src/views/framework/login/sms-code.vue +++ b/packages/ui/certd-client/src/views/framework/login/sms-code.vue @@ -23,6 +23,7 @@ const props = defineProps<{ phoneCode?: string; imgCode?: string; randomStr?: string; + verificationType?: string; }>(); const emit = defineEmits(["update:value", "change"]); @@ -58,6 +59,7 @@ async function sendSmsCode() { mobile: props.mobile, imgCode: props.imgCode, randomStr: props.randomStr, + verificationType: props.verificationType, }); } finally { loading.value = false; diff --git a/packages/ui/certd-client/src/views/framework/register/email-code.vue b/packages/ui/certd-client/src/views/framework/register/email-code.vue index 51e6cd15..6ee92e1f 100644 --- a/packages/ui/certd-client/src/views/framework/register/email-code.vue +++ b/packages/ui/certd-client/src/views/framework/register/email-code.vue @@ -22,6 +22,7 @@ const props = defineProps<{ email?: string; imgCode?: string; randomStr?: string; + verificationType?: string; }>(); const emit = defineEmits(["update:value", "change"]); @@ -53,6 +54,7 @@ async function sendSmsCode() { email: props.email, imgCode: props.imgCode, randomStr: props.randomStr, + verificationType: props.verificationType, }); } finally { loading.value = false; diff --git a/packages/ui/certd-client/src/views/sys/settings/tabs/base.vue b/packages/ui/certd-client/src/views/sys/settings/tabs/base.vue index a3024ae4..8d2138b6 100644 --- a/packages/ui/certd-client/src/views/sys/settings/tabs/base.vue +++ b/packages/ui/certd-client/src/views/sys/settings/tabs/base.vue @@ -47,6 +47,10 @@
+ + + + {{ t("certd.saveButton") }} diff --git a/packages/ui/certd-server/src/controller/basic/code-controller.ts b/packages/ui/certd-server/src/controller/basic/code-controller.ts index 09a90009..fb0eedc2 100644 --- a/packages/ui/certd-server/src/controller/basic/code-controller.ts +++ b/packages/ui/certd-server/src/controller/basic/code-controller.ts @@ -27,6 +27,9 @@ export class EmailCodeReq { @Rule(RuleType.string().required().max(4)) imgCode: string; + + @Rule(RuleType.string()) + verificationType: string; } /** @@ -55,8 +58,20 @@ export class BasicController extends BaseController { @Body(ALL) 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.sendEmailCode(body.email, body.randomStr); + await this.codeService.sendEmailCode(body.email, body.randomStr, opts); // 设置缓存内容 return this.ok(null); } diff --git a/packages/ui/certd-server/src/controller/user/login/forgot-password-controller.ts b/packages/ui/certd-server/src/controller/user/login/forgot-password-controller.ts new file mode 100644 index 00000000..a497504b --- /dev/null +++ b/packages/ui/certd-server/src/controller/user/login/forgot-password-controller.ts @@ -0,0 +1,56 @@ +import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core'; +import { BaseController, CommonException, Constants, SysSettingsService } from "@certd/lib-server"; +import { CodeService } from '../../../modules/basic/service/code-service.js'; +import { UserService } from '../../../modules/sys/authority/service/user-service.js'; +import { LoginService } from "../../../modules/login/service/login-service.js"; + +/** + */ +@Provide() +@Controller('/api') +export class LoginController extends BaseController { + @Inject() + loginService: LoginService; + @Inject() + userService: UserService; + @Inject() + codeService: CodeService; + + @Inject() + sysSettingsService: SysSettingsService; + + @Post('/forgotPassword', { summary: Constants.per.guest }) + public async forgotPassword( + @Body(ALL) + body: any, + ) { + const sysSettings = await this.sysSettingsService.getPublicSettings(); + if(!sysSettings.selfServicePasswordRetrievalEnabled) { + throw new CommonException('暂未开启自助找回'); + } + + if(body.type === 'email') { + this.codeService.checkEmailCode({ + verificationType: 'forgotPassword', + email: body.input, + randomStr: body.randomStr, + validateCode: body.validateCode, + throwError: true, + }); + } else if(body.type === 'mobile') { + await this.codeService.checkSmsCode({ + verificationType: 'forgotPassword', + mobile: body.input, + randomStr: body.randomStr, + phoneCode: body.phoneCode, + smsCode: body.validateCode, + throwError: true, + }); + } else { + throw new CommonException('暂不支持的找回类型,请联系管理员找回'); + } + const username = await this.userService.forgotPassword(body); + username && this.loginService.clearCacheOnSuccess(username) + return this.ok(); + } +} diff --git a/packages/ui/certd-server/src/modules/basic/service/code-service.ts b/packages/ui/certd-server/src/modules/basic/service/code-service.ts index a7f5122a..62d108ca 100644 --- a/packages/ui/certd-server/src/modules/basic/service/code-service.ts +++ b/packages/ui/certd-server/src/modules/basic/service/code-service.ts @@ -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) { throw new Error('手机号不能为空'); } @@ -65,6 +73,8 @@ export class CodeService { throw new Error('randomStr不能为空'); } + const duration = Math.max(Math.floor(Math.min(opts?.duration || 5, 15)), 1); + const sysSettings = await this.sysSettingsService.getPrivateSettings(); if (!sysSettings.sms?.config?.accessId) { throw new Error('当前站点还未配置短信'); @@ -84,16 +94,29 @@ export class CodeService { phoneCode, }); - const key = this.buildSmsCodeKey(phoneCode, mobile, randomStr); + const key = this.buildSmsCodeKey(phoneCode, mobile, randomStr, opts?.verificationType); cache.set(key, smsCode, { - ttl: 5 * 60 * 1000, //5分钟 + ttl: duration * 60 * 1000, //5分钟 }); 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) { throw new Error('Email不能为空'); } @@ -110,15 +133,20 @@ export class CodeService { } 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({ - subject: `【${siteTitle}】验证码`, - content: `您的验证码是${code},请勿泄露`, + subject: title, + content: content, receivers: [email], }); - const key = this.buildEmailCodeKey(email, randomStr); + const key = this.buildEmailCodeKey(email, randomStr, opts?.verificationType); cache.set(key, code, { - ttl: 5 * 60 * 1000, //5分钟 + ttl: duration * 60 * 1000, //5分钟 }); return code; } @@ -126,20 +154,20 @@ export class CodeService { /** * checkSms */ - async checkSmsCode(opts: { mobile: string; phoneCode: string; smsCode: string; randomStr: string; throwError: boolean }) { - const key = this.buildSmsCodeKey(opts.phoneCode, opts.mobile, opts.randomStr); + 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, opts.verificationType); if (isDev()) { return true; } return this.checkValidateCode(key, opts.smsCode, opts.throwError); } - buildSmsCodeKey(phoneCode: string, mobile: string, randomStr: string) { - return `sms:${phoneCode}${mobile}:${randomStr}`; + buildSmsCodeKey(phoneCode: string, mobile: string, randomStr: string, verificationType?: string) { + return ['sms', verificationType, phoneCode, mobile, randomStr].filter(item => !!item).join(':'); } - buildEmailCodeKey(email: string, randomStr: string) { - return `email:${email}:${randomStr}`; + buildEmailCodeKey(email: string, randomStr: string, verificationType?: string) { + return ['email', verificationType, email, randomStr].filter(item => !!item).join(':'); } checkValidateCode(key: string, userCode: string, throwError = true) { //验证图片验证码 @@ -154,8 +182,18 @@ export class CodeService { return true; } - checkEmailCode(opts: { randomStr: string; validateCode: string; email: string; throwError: boolean }) { - const key = this.buildEmailCodeKey(opts.email, opts.randomStr); + checkEmailCode(opts: { randomStr: string; validateCode: string; email: string; verificationType?: string; throwError: boolean }) { + const key = this.buildEmailCodeKey(opts.email, opts.randomStr, opts.verificationType); return this.checkValidateCode(key, opts.validateCode, opts.throwError); } + + compile(templateString: string) { + return new Function( + "data", + ` with(data || {}) { + return \`${templateString}\`; + } + ` + ); + } } diff --git a/packages/ui/certd-server/src/modules/sys/authority/service/user-service.ts b/packages/ui/certd-server/src/modules/sys/authority/service/user-service.ts index fec66ac4..3298d477 100644 --- a/packages/ui/certd-server/src/modules/sys/authority/service/user-service.ts +++ b/packages/ui/certd-server/src/modules/sys/authority/service/user-service.ts @@ -15,6 +15,7 @@ import { DbAdapter } from '../../../db/index.js'; import { simpleNanoId, utils } from '@certd/basic'; export type RegisterType = 'username' | 'mobile' | 'email'; +export type ForgotPasswordType = 'mobile' | 'email'; export const AdminRoleId = 1 /** @@ -23,7 +24,7 @@ export const AdminRoleId = 1 @Provide() @Scope(ScopeEnum.Request, { allowDowngrade: true }) export class UserService extends BaseService { - + @InjectEntityModel(UserEntity) repository: Repository; @Inject() @@ -229,6 +230,29 @@ export class UserService extends BaseService { return newUser; } + async forgotPassword( + data: { + type: ForgotPasswordType; input?: string, phoneCode?: string, + randomStr: string, imgCode:string, validateCode: string, + password: string, confirmPassword: string, + } + ) { + if(!data.type) { + throw new CommonException('找回类型不能为空'); + } + if(data.password !== data.confirmPassword) { + throw new CommonException('两次输入的密码不一致'); + } + const user = await this.findOne([{ [data.type]: data.input }]); + console.log('user', user) + if(!user) { + throw new CommonException('用户不存在'); + // return; + } + await this.resetPassword(user.id, data.password) + return user.username; + } + async changePassword(userId: any, form: any) { const user = await this.info(userId); const passwordChecked = await this.checkPassword(form.password, user.password, user.passwordVersion);