mirror of https://github.com/certd/certd
perf: 新增找回密码功能 @nicheng-he
* feat 找回密码 * 1.发送邮件时修改模版 2.重置成功时清除登陆错误次数 * 增加自助找回密码控制 * 补充接口自助找回判断v2
parent
b33ec201ac
commit
81ac240ac8
|
@ -22,6 +22,7 @@ export class SysPublicSettings extends BaseSettings {
|
||||||
mobileRegisterEnabled = false;
|
mobileRegisterEnabled = false;
|
||||||
smsLoginEnabled = false;
|
smsLoginEnabled = false;
|
||||||
emailRegisterEnabled = false;
|
emailRegisterEnabled = false;
|
||||||
|
selfServicePasswordRetrievalEnabled = false;
|
||||||
|
|
||||||
limitUserPipelineCount = 0;
|
limitUserPipelineCount = 0;
|
||||||
managerOtherUserPipeline = false;
|
managerOtherUserPipeline = false;
|
||||||
|
|
|
@ -57,6 +57,7 @@ export default {
|
||||||
passwordPlaceholder: "Please enter your password",
|
passwordPlaceholder: "Please enter your password",
|
||||||
mobilePlaceholder: "Please enter your mobile number",
|
mobilePlaceholder: "Please enter your mobile number",
|
||||||
loginButton: "Log In",
|
loginButton: "Log In",
|
||||||
|
forgotPassword: "Forgot password?",
|
||||||
forgotAdminPassword: "Forgot admin password?",
|
forgotAdminPassword: "Forgot admin password?",
|
||||||
registerLink: "Register",
|
registerLink: "Register",
|
||||||
|
|
||||||
|
|
|
@ -565,6 +565,7 @@ export default {
|
||||||
dualStackNetworkHelper: "If IPv6 priority is selected, enable IPv6 in docker-compose.yaml",
|
dualStackNetworkHelper: "If IPv6 priority is selected, enable IPv6 in docker-compose.yaml",
|
||||||
enableCommonCnameService: "Enable Public CNAME Service",
|
enableCommonCnameService: "Enable Public CNAME Service",
|
||||||
commonCnameHelper: "Allow use of public CNAME service. If disabled and no <router-link to='/sys/cname/provider'>custom CNAME service</router-link> is set, CNAME proxy certificate application will not work.",
|
commonCnameHelper: "Allow use of public CNAME service. If disabled and no <router-link to='/sys/cname/provider'>custom CNAME service</router-link> is set, CNAME proxy certificate application will not work.",
|
||||||
|
enableCommonSelfServicePasswordRetrieval: "Enable self-service password recovery",
|
||||||
saveButton: "Save",
|
saveButton: "Save",
|
||||||
stopSuccess: "Stopped successfully",
|
stopSuccess: "Stopped successfully",
|
||||||
google: "Google",
|
google: "Google",
|
||||||
|
|
|
@ -57,6 +57,7 @@ export default {
|
||||||
passwordPlaceholder: "请输入密码",
|
passwordPlaceholder: "请输入密码",
|
||||||
mobilePlaceholder: "请输入手机号",
|
mobilePlaceholder: "请输入手机号",
|
||||||
loginButton: "登录",
|
loginButton: "登录",
|
||||||
|
forgotPassword: "忘记密码?",
|
||||||
forgotAdminPassword: "忘记管理员密码?",
|
forgotAdminPassword: "忘记管理员密码?",
|
||||||
registerLink: "注册",
|
registerLink: "注册",
|
||||||
|
|
||||||
|
|
|
@ -571,6 +571,7 @@ export default {
|
||||||
dualStackNetworkHelper: "如果选择IPv6优先,需要在docker-compose.yaml中启用ipv6",
|
dualStackNetworkHelper: "如果选择IPv6优先,需要在docker-compose.yaml中启用ipv6",
|
||||||
enableCommonCnameService: "启用公共CNAME服务",
|
enableCommonCnameService: "启用公共CNAME服务",
|
||||||
commonCnameHelper: "是否可以使用公共CNAME服务,如果禁用,且没有设置<router-link to='/sys/cname/provider'>自定义CNAME服务</router-link>,则无法使用CNAME代理方式申请证书",
|
commonCnameHelper: "是否可以使用公共CNAME服务,如果禁用,且没有设置<router-link to='/sys/cname/provider'>自定义CNAME服务</router-link>,则无法使用CNAME代理方式申请证书",
|
||||||
|
enableCommonSelfServicePasswordRetrieval: "启用自助找回密码",
|
||||||
saveButton: "保存",
|
saveButton: "保存",
|
||||||
stopSuccess: "停止成功",
|
stopSuccess: "停止成功",
|
||||||
google: "Google",
|
google: "Google",
|
||||||
|
|
|
@ -24,6 +24,14 @@ export const outsideResource = [
|
||||||
path: "/register",
|
path: "/register",
|
||||||
component: "/framework/register/index.vue",
|
component: "/framework/register/index.vue",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
title: "找回密码",
|
||||||
|
},
|
||||||
|
name: "forgotPassword",
|
||||||
|
path: "/forgotPassword",
|
||||||
|
component: "/framework/forgot-password/index.vue",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...errorPage,
|
...errorPage,
|
||||||
|
|
|
@ -36,6 +36,7 @@ export type SysPublicSetting = {
|
||||||
emailRegisterEnabled?: boolean;
|
emailRegisterEnabled?: boolean;
|
||||||
passwordLoginEnabled?: boolean;
|
passwordLoginEnabled?: boolean;
|
||||||
smsLoginEnabled?: boolean;
|
smsLoginEnabled?: boolean;
|
||||||
|
selfServicePasswordRetrievalEnabled?: boolean;
|
||||||
|
|
||||||
limitUserPipelineCount?: number;
|
limitUserPipelineCount?: number;
|
||||||
managerOtherUserPipeline?: boolean;
|
managerOtherUserPipeline?: boolean;
|
||||||
|
|
|
@ -20,6 +20,17 @@ export interface SmsLoginReq {
|
||||||
randomStr: string;
|
randomStr: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ForgotPasswordReq {
|
||||||
|
forgotPasswordType: string;
|
||||||
|
input: string;
|
||||||
|
randomStr: string;
|
||||||
|
imgCode: string;
|
||||||
|
validateCode: string;
|
||||||
|
|
||||||
|
password: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserInfoRes {
|
export interface UserInfoRes {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -43,6 +54,13 @@ export async function register(user: RegisterReq): Promise<UserInfoRes> {
|
||||||
data: user,
|
data: user,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
export async function forgotPassword(data: ForgotPasswordReq): Promise<any> {
|
||||||
|
return await request({
|
||||||
|
url: "/forgotPassword",
|
||||||
|
method: "post",
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
export async function logout() {
|
export async function logout() {
|
||||||
return await request({
|
return await request({
|
||||||
url: "/logout",
|
url: "/logout",
|
||||||
|
|
|
@ -4,7 +4,7 @@ import router from "../../router";
|
||||||
import { LocalStorage } from "/src/utils/util.storage";
|
import { LocalStorage } from "/src/utils/util.storage";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import * as UserApi from "./api.user";
|
import * as UserApi from "./api.user";
|
||||||
import { RegisterReq, SmsLoginReq } from "./api.user";
|
import { ForgotPasswordReq, RegisterReq, SmsLoginReq } from "./api.user";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { LoginReq, UserInfoRes } from "/@/store/user/api.user";
|
import { LoginReq, UserInfoRes } from "/@/store/user/api.user";
|
||||||
import { message, Modal, notification } from "ant-design-vue";
|
import { message, Modal, notification } from "ant-design-vue";
|
||||||
|
@ -67,6 +67,13 @@ export const useUserStore = defineStore({
|
||||||
});
|
});
|
||||||
await router.replace("/login");
|
await router.replace("/login");
|
||||||
},
|
},
|
||||||
|
async forgotPassword(params: ForgotPasswordReq): Promise<any> {
|
||||||
|
await UserApi.forgotPassword(params);
|
||||||
|
notification.success({
|
||||||
|
message: "密码已重置,请登录",
|
||||||
|
});
|
||||||
|
await router.replace("/login");
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* @description: login
|
* @description: login
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
<template>
|
||||||
|
<div class="main forgot-password-page">
|
||||||
|
<a-form
|
||||||
|
ref="formRef"
|
||||||
|
class="user-layout-forgot-password"
|
||||||
|
name="custom-validation"
|
||||||
|
:model="formState"
|
||||||
|
:rules="rules"
|
||||||
|
v-bind="layout"
|
||||||
|
:label-col="{ span: 6 }"
|
||||||
|
@finish="handleFinish"
|
||||||
|
@finish-failed="handleFinishFailed"
|
||||||
|
>
|
||||||
|
<a-tabs v-model:active-key="forgotPasswordType" :destroyInactiveTabPane="true">
|
||||||
|
<a-tab-pane key="email" tab="邮箱找回">
|
||||||
|
<a-form-item has-feedback name="input" label="邮箱">
|
||||||
|
<a-input v-model:value="formState.input" placeholder="邮箱" size="large" autocomplete="off">
|
||||||
|
<template #prefix>
|
||||||
|
<fs-icon icon="ion:mail-outline"></fs-icon>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<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"
|
||||||
|
verification-type="forgotPassword"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="mobile" tab="手机号找回">
|
||||||
|
<a-form-item required has-feedback name="input" label="手机号">
|
||||||
|
<a-input v-model:value="formState.input" placeholder="手机号" autocomplete="off">
|
||||||
|
<template #prefix>
|
||||||
|
<fs-icon icon="ion:phone-portrait-outline"></fs-icon>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<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"
|
||||||
|
verification-type="forgotPassword"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
|
||||||
|
<a-form-item has-feedback name="imgCode" label="图片验证码">
|
||||||
|
<image-code ref="imageCodeRef" v-model:value="formState.imgCode" v-model:random-str="formState.randomStr"></image-code>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item has-feedback name="password" label="新密码">
|
||||||
|
<a-input-password v-model:value="formState.password" placeholder="新密码" size="large" autocomplete="off">
|
||||||
|
<template #prefix>
|
||||||
|
<fs-icon icon="ion:lock-closed-outline"></fs-icon>
|
||||||
|
</template>
|
||||||
|
</a-input-password>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item has-feedback name="confirmPassword" label="确认密码">
|
||||||
|
<a-input-password v-model:value="formState.confirmPassword" placeholder="确认密码" size="large" autocomplete="off">
|
||||||
|
<template #prefix>
|
||||||
|
<fs-icon icon="ion:lock-closed-outline"></fs-icon>
|
||||||
|
</template>
|
||||||
|
</a-input-password>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-button type="primary" size="large" html-type="submit" class="submit-button"> 找回密码</a-button>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href="https://certd.docmirror.cn/guide/use/forgotpasswd/" target="_blank"> 管理员无绑定通信方式或MFA丢失找回 </a>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, reactive, ref, toRaw, watch } from "vue";
|
||||||
|
import ImageCode from "/@/views/framework/login/image-code.vue";
|
||||||
|
import EmailCode from "/@/views/framework/register/email-code.vue";
|
||||||
|
import SmsCode from "/@/views/framework/login/sms-code.vue";
|
||||||
|
import { utils } from "@fast-crud/fast-crud";
|
||||||
|
import { useUserStore } from "/@/store/user";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "ForgotPasswordPage",
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
input: [{ required: true }],
|
||||||
|
validateCode: [{ required: true }],
|
||||||
|
imgCode: [{ required: true }, { min: 4, max: 4, message: "请输入4位图片验证码" }],
|
||||||
|
password: [
|
||||||
|
{ required: true, trigger: "change", message: "请输入密码" },
|
||||||
|
{ min: 6, message: "至少输入6位密码" },
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, trigger: "change", message: "请确认密码" },
|
||||||
|
{
|
||||||
|
validator: async (rule: any, value: any) => {
|
||||||
|
if (value && value !== formState.password) {
|
||||||
|
throw new Error("两次输入密码不一致");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const layout = {
|
||||||
|
labelCol: {
|
||||||
|
span: 0,
|
||||||
|
},
|
||||||
|
wrapperCol: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const forgotPasswordType = ref();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const formRef = ref();
|
||||||
|
const imageCodeRef = ref();
|
||||||
|
|
||||||
|
const formState: any = reactive({
|
||||||
|
input: "",
|
||||||
|
randomStr: "",
|
||||||
|
imgCode: "",
|
||||||
|
phoneCode: "86",
|
||||||
|
validateCode: "",
|
||||||
|
|
||||||
|
password: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO 这里配置不同的找回方式
|
||||||
|
onMounted(() => {
|
||||||
|
forgotPasswordType.value = "email";
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监控找回类型变化
|
||||||
|
watch(forgotPasswordType, () => {
|
||||||
|
formState.input = "";
|
||||||
|
formState.validateCode = "";
|
||||||
|
imageCodeRef.value.resetImageCode();
|
||||||
|
formRef.value.clearValidate(Object.keys(formState).filter(key => !["password", "confirmPassword"].includes(key)));
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFinish = async (values: any) => {
|
||||||
|
await userStore.forgotPassword(
|
||||||
|
toRaw({
|
||||||
|
type: forgotPasswordType.value,
|
||||||
|
input: formState.input,
|
||||||
|
randomStr: formState.randomStr,
|
||||||
|
imgCode: formState.imgCode,
|
||||||
|
validateCode: formState.validateCode,
|
||||||
|
password: formState.password,
|
||||||
|
confirmPassword: formState.confirmPassword,
|
||||||
|
}) as any
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinishFailed = (errors: any) => {
|
||||||
|
utils.logger.log(errors);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped lang="less">
|
||||||
|
.forgot-password-page {
|
||||||
|
.user-layout-forgot-password {
|
||||||
|
.submit-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -11,7 +11,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, useAttrs } from "vue";
|
import { ref, useAttrs, defineExpose } from "vue";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -32,5 +32,10 @@ function resetImageCode() {
|
||||||
imageCodeUrl.value = url + "?randomStr=" + randomStr;
|
imageCodeUrl.value = url + "?randomStr=" + randomStr;
|
||||||
emit("update:randomStr", randomStr);
|
emit("update:randomStr", randomStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
resetImageCode,
|
||||||
|
})
|
||||||
|
|
||||||
resetImageCode();
|
resetImageCode();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -47,10 +47,10 @@
|
||||||
{{ t("authentication.loginButton") }}
|
{{ t("authentication.loginButton") }}
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|
||||||
<div v-if="!settingStore.isComm" class="mt-2">
|
<div v-if="!!settingStore.sysPublic.selfServicePasswordRetrievalEnabled" class="mt-2">
|
||||||
<a href="https://certd.docmirror.cn/guide/use/forgotpasswd/" target="_blank">
|
<router-link :to="{ name: 'forgotPassword' }">
|
||||||
{{ t("authentication.forgotAdminPassword") }}
|
{{ t("authentication.forgotPassword") }}
|
||||||
</a>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -47,6 +47,10 @@
|
||||||
<div class="helper" v-html="t('certd.commonCnameHelper')"></div>
|
<div class="helper" v-html="t('certd.commonCnameHelper')"></div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item :label="t('certd.enableCommonSelfServicePasswordRetrieval')" :name="['public', 'selfServicePasswordRetrievalEnabled']">
|
||||||
|
<a-switch v-model:checked="formState.public.selfServicePasswordRetrievalEnabled" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
|
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
|
||||||
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
|
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}\`;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { DbAdapter } from '../../../db/index.js';
|
||||||
import { simpleNanoId, utils } from '@certd/basic';
|
import { simpleNanoId, utils } from '@certd/basic';
|
||||||
|
|
||||||
export type RegisterType = 'username' | 'mobile' | 'email';
|
export type RegisterType = 'username' | 'mobile' | 'email';
|
||||||
|
export type ForgotPasswordType = 'mobile' | 'email';
|
||||||
|
|
||||||
export const AdminRoleId = 1
|
export const AdminRoleId = 1
|
||||||
/**
|
/**
|
||||||
|
@ -23,7 +24,7 @@ export const AdminRoleId = 1
|
||||||
@Provide()
|
@Provide()
|
||||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||||
export class UserService extends BaseService<UserEntity> {
|
export class UserService extends BaseService<UserEntity> {
|
||||||
|
|
||||||
@InjectEntityModel(UserEntity)
|
@InjectEntityModel(UserEntity)
|
||||||
repository: Repository<UserEntity>;
|
repository: Repository<UserEntity>;
|
||||||
@Inject()
|
@Inject()
|
||||||
|
@ -229,6 +230,29 @@ export class UserService extends BaseService<UserEntity> {
|
||||||
return newUser;
|
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) {
|
async changePassword(userId: any, form: any) {
|
||||||
const user = await this.info(userId);
|
const user = await this.info(userId);
|
||||||
const passwordChecked = await this.checkPassword(form.password, user.password, user.passwordVersion);
|
const passwordChecked = await this.checkPassword(form.password, user.password, user.passwordVersion);
|
||||||
|
|
Loading…
Reference in New Issue