mirror of
https://github.com/certd/certd.git
synced 2026-04-24 12:27:25 +08:00
perf: 新增找回密码功能 @nicheng-he
* feat 找回密码 * 1.发送邮件时修改模版 2.重置成功时清除登陆错误次数 * 增加自助找回密码控制 * 补充接口自助找回判断
This commit is contained in:
@@ -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",
|
||||
|
||||
|
||||
@@ -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 <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",
|
||||
stopSuccess: "Stopped successfully",
|
||||
google: "Google",
|
||||
|
||||
@@ -57,6 +57,7 @@ export default {
|
||||
passwordPlaceholder: "请输入密码",
|
||||
mobilePlaceholder: "请输入手机号",
|
||||
loginButton: "登录",
|
||||
forgotPassword: "忘记密码?",
|
||||
forgotAdminPassword: "忘记管理员密码?",
|
||||
registerLink: "注册",
|
||||
|
||||
|
||||
@@ -571,6 +571,7 @@ export default {
|
||||
dualStackNetworkHelper: "如果选择IPv6优先,需要在docker-compose.yaml中启用ipv6",
|
||||
enableCommonCnameService: "启用公共CNAME服务",
|
||||
commonCnameHelper: "是否可以使用公共CNAME服务,如果禁用,且没有设置<router-link to='/sys/cname/provider'>自定义CNAME服务</router-link>,则无法使用CNAME代理方式申请证书",
|
||||
enableCommonSelfServicePasswordRetrieval: "启用自助找回密码",
|
||||
saveButton: "保存",
|
||||
stopSuccess: "停止成功",
|
||||
google: "Google",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -36,6 +36,7 @@ export type SysPublicSetting = {
|
||||
emailRegisterEnabled?: boolean;
|
||||
passwordLoginEnabled?: boolean;
|
||||
smsLoginEnabled?: boolean;
|
||||
selfServicePasswordRetrievalEnabled?: boolean;
|
||||
|
||||
limitUserPipelineCount?: number;
|
||||
managerOtherUserPipeline?: boolean;
|
||||
|
||||
@@ -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<UserInfoRes> {
|
||||
data: user,
|
||||
});
|
||||
}
|
||||
export async function forgotPassword(data: ForgotPasswordReq): Promise<any> {
|
||||
return await request({
|
||||
url: "/forgotPassword",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
export async function logout() {
|
||||
return await request({
|
||||
url: "/logout",
|
||||
|
||||
@@ -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<any> {
|
||||
await UserApi.forgotPassword(params);
|
||||
notification.success({
|
||||
message: "密码已重置,请登录",
|
||||
});
|
||||
await router.replace("/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>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, useAttrs } from "vue";
|
||||
import { ref, useAttrs, defineExpose } from "vue";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -32,5 +32,10 @@ function resetImageCode() {
|
||||
imageCodeUrl.value = url + "?randomStr=" + randomStr;
|
||||
emit("update:randomStr", randomStr);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetImageCode,
|
||||
})
|
||||
|
||||
resetImageCode();
|
||||
</script>
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
{{ t("authentication.loginButton") }}
|
||||
</a-button>
|
||||
|
||||
<div v-if="!settingStore.isComm" class="mt-2">
|
||||
<a href="https://certd.docmirror.cn/guide/use/forgotpasswd/" target="_blank">
|
||||
{{ t("authentication.forgotAdminPassword") }}
|
||||
</a>
|
||||
<div v-if="!!settingStore.sysPublic.selfServicePasswordRetrievalEnabled" class="mt-2">
|
||||
<router-link :to="{ name: 'forgotPassword' }">
|
||||
{{ t("authentication.forgotPassword") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
<div class="helper" v-html="t('certd.commonCnameHelper')"></div>
|
||||
</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-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
|
||||
</a-form-item>
|
||||
|
||||
Reference in New Issue
Block a user