mirror of
https://github.com/certd/certd.git
synced 2026-05-17 22:07:34 +08:00
perf(用户资料): 新增手机号邮箱绑定功能
实现用户邮箱和手机号的绑定与修改功能,包括: 1. 添加联系方式绑定API接口 2. 实现身份验证流程 3. 添加前端绑定对话框组件 4. 完善用户资料页面的联系方式展示和编辑入口 5. 添加联系方式冲突检测逻辑 6. 实现验证码校验功能
This commit is contained in:
@@ -23,6 +23,37 @@ export async function UpdateProfile(form: any) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function GetContactCapability() {
|
||||||
|
return await request({
|
||||||
|
url: "/mine/contact/capability",
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function UpdateMobile(form: any) {
|
||||||
|
return await request({
|
||||||
|
url: "/mine/contact/mobile",
|
||||||
|
method: "POST",
|
||||||
|
data: form,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function VerifyContactIdentity(form: any) {
|
||||||
|
return await request({
|
||||||
|
url: "/mine/contact/verifyIdentity",
|
||||||
|
method: "POST",
|
||||||
|
data: form,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function UpdateEmail(form: any) {
|
||||||
|
return await request({
|
||||||
|
url: "/mine/contact/email",
|
||||||
|
method: "POST",
|
||||||
|
data: form,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function GetOauthBounds() {
|
export async function GetOauthBounds() {
|
||||||
return await request({
|
return await request({
|
||||||
url: "/oauth/bounds",
|
url: "/oauth/bounds",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { defineComponent } from "vue";
|
||||||
|
import SmsCode from "/@/views/framework/login/sms-code.vue";
|
||||||
|
import EmailCode from "/@/views/framework/register/email-code.vue";
|
||||||
|
|
||||||
|
export const ContactCodeInput = defineComponent({
|
||||||
|
name: "ContactCodeInput",
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["update:modelValue"],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const onChange = (value: string) => emit("update:modelValue", value);
|
||||||
|
return () => {
|
||||||
|
if (props.type === "email") {
|
||||||
|
return <EmailCode value={props.modelValue} captcha={props.form.contactCaptcha} email={props.form.email} verificationType="bindEmail" onUpdate:value={onChange} />;
|
||||||
|
}
|
||||||
|
return <SmsCode value={props.modelValue} captcha={props.form.contactCaptcha} mobile={props.form.mobile} phoneCode={props.form.phoneCode} verificationType="bindMobile" onUpdate:value={onChange} />;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { defineComponent } from "vue";
|
||||||
|
import SmsCode from "/@/views/framework/login/sms-code.vue";
|
||||||
|
import EmailCode from "/@/views/framework/register/email-code.vue";
|
||||||
|
|
||||||
|
export const IdentityCodeInput = defineComponent({
|
||||||
|
name: "IdentityCodeInput",
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
userInfo: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["update:modelValue"],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const onChange = (value: string) => emit("update:modelValue", value);
|
||||||
|
return () => {
|
||||||
|
if (props.form.identityType === "email") {
|
||||||
|
return <EmailCode value={props.modelValue} captcha={props.form.identityCaptcha} email={props.userInfo.email} verificationType="contactIdentity" onUpdate:value={onChange} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SmsCode value={props.modelValue} captcha={props.form.identityCaptcha} mobile={props.userInfo.mobile} phoneCode={props.userInfo.phoneCode || "86"} verificationType="contactIdentity" onUpdate:value={onChange} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
// useUserProfile, 获取 openEditProfileDialog ,参考 useTemplate方法
|
// useUserProfile, 获取 openEditProfileDialog ,参考 useTemplate方法
|
||||||
import { useFormWrapper } from "@fast-crud/fast-crud";
|
import { compute, dict } from "@fast-crud/fast-crud";
|
||||||
import { ref } from "vue";
|
|
||||||
import { cloneDeep, merge } from "lodash-es";
|
|
||||||
|
|
||||||
// 假设的 API 导入
|
// 假设的 API 导入
|
||||||
import * as userProfileApi from "./api";
|
import * as userProfileApi from "./api";
|
||||||
import { useUserStore } from "/@/store/user";
|
import { useUserStore } from "/@/store/user";
|
||||||
import { useI18n } from "/src/locales";
|
import { useI18n } from "/src/locales";
|
||||||
|
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
import { ContactCodeInput } from "./contact-code-input";
|
||||||
|
import { IdentityCodeInput } from "./identity-code-input";
|
||||||
|
import { useFormDialog } from "/@/use/use-dialog";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户资料编辑相关功能
|
* 获取用户资料编辑相关功能
|
||||||
* @returns {{openEditProfileDialog: openEditProfileDialog}}
|
* @returns {{openEditProfileDialog: openEditProfileDialog}}
|
||||||
*/
|
*/
|
||||||
export function useUserProfile() {
|
export function useUserProfile() {
|
||||||
const { openCrudFormDialog } = useFormWrapper();
|
const { openFormDialog } = useFormDialog();
|
||||||
const wrapperRef = ref();
|
|
||||||
async function openEditProfileDialog(req: { onUpdated?: (ctx: any) => void }) {
|
async function openEditProfileDialog(req: { onUpdated?: (ctx: any) => void }) {
|
||||||
const detail = await userProfileApi.getMineInfo();
|
const detail = await userProfileApi.getMineInfo();
|
||||||
if (!detail) {
|
if (!detail) {
|
||||||
@@ -24,31 +26,28 @@ export function useUserProfile() {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const userProfileFormRef = ref();
|
async function doSubmit(form: any) {
|
||||||
async function doSubmit(opts: { form: any }) {
|
|
||||||
const form = opts.form;
|
|
||||||
const { id } = await userProfileApi.UpdateProfile(form);
|
const { id } = await userProfileApi.UpdateProfile(form);
|
||||||
if (req.onUpdated) {
|
if (req.onUpdated) {
|
||||||
req.onUpdated({ id });
|
req.onUpdated({ id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const crudOptions: any = {
|
await openFormDialog({
|
||||||
form: {
|
title: `编辑用户资料`,
|
||||||
doSubmit,
|
wrapper: {
|
||||||
wrapper: {
|
width: 600,
|
||||||
title: `编辑用户资料`,
|
|
||||||
width: 1100,
|
|
||||||
onOpened(opts: { form: any }) {
|
|
||||||
merge(opts.form, detail);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
initialForm: detail,
|
||||||
|
onSubmit: doSubmit,
|
||||||
columns: {
|
columns: {
|
||||||
nickName: {
|
nickName: {
|
||||||
title: t("certd.nickName"),
|
title: t("certd.nickName"),
|
||||||
type: "text",
|
type: "text",
|
||||||
form: {
|
form: {
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
component: {
|
component: {
|
||||||
placeholder: t("certd.nickName"),
|
placeholder: t("certd.nickName"),
|
||||||
},
|
},
|
||||||
@@ -71,6 +70,9 @@ export function useUserProfile() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
component: {
|
component: {
|
||||||
vModel: "modelValue",
|
vModel: "modelValue",
|
||||||
valueType: "key",
|
valueType: "key",
|
||||||
@@ -98,10 +100,7 @@ export function useUserProfile() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const wrapper = await openCrudFormDialog({ crudOptions });
|
|
||||||
wrapperRef.value = wrapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -110,26 +109,20 @@ export function useUserProfile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function usePasskeyRegister() {
|
export function usePasskeyRegister() {
|
||||||
const { openCrudFormDialog } = useFormWrapper();
|
const { openFormDialog } = useFormDialog();
|
||||||
const wrapperRef = ref();
|
|
||||||
async function openRegisterDialog(req: { onSubmit?: (ctx: any) => void }) {
|
async function openRegisterDialog(req: { onSubmit?: (ctx: any) => void }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const userStore = useUserStore();
|
await openFormDialog({
|
||||||
const deviceNameRef = ref();
|
title: t("authentication.registerPasskey"),
|
||||||
|
wrapper: {
|
||||||
const crudOptions: any = {
|
width: 500,
|
||||||
form: {
|
},
|
||||||
wrapper: {
|
initialForm: {
|
||||||
title: t("authentication.registerPasskey"),
|
deviceName: "",
|
||||||
width: 500,
|
},
|
||||||
onOpened(opts: { form: any }) {
|
onSubmit: async (form: any) => {
|
||||||
opts.form.deviceName = "";
|
await req.onSubmit?.({ form });
|
||||||
},
|
|
||||||
},
|
|
||||||
onSubmit: req.onSubmit,
|
|
||||||
afterSubmit: null,
|
|
||||||
onSuccess: null,
|
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
deviceName: {
|
deviceName: {
|
||||||
@@ -147,15 +140,229 @@ export function usePasskeyRegister() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const wrapper = await openCrudFormDialog({ crudOptions });
|
|
||||||
wrapperRef.value = wrapper;
|
|
||||||
|
|
||||||
return wrapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openRegisterDialog,
|
openRegisterDialog,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useContactBind() {
|
||||||
|
const { openFormDialog } = useFormDialog();
|
||||||
|
|
||||||
|
async function openContactBindDialog(req: { type: "mobile" | "email"; userInfo: any; contactCapability: { smsEnabled?: boolean }; onUpdated?: () => Promise<void> | void }) {
|
||||||
|
const methods = [{ label: "密码", value: "password" }];
|
||||||
|
if (req.userInfo.email) {
|
||||||
|
methods.push({ label: "邮箱", value: "email" });
|
||||||
|
}
|
||||||
|
if (req.contactCapability.smsEnabled && req.userInfo.mobile) {
|
||||||
|
methods.push({ label: "手机号", value: "mobile" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openChangeDialog(identityValidationCode: string) {
|
||||||
|
const isMobile = req.type === "mobile";
|
||||||
|
await openFormDialog({
|
||||||
|
title: isMobile ? (req.userInfo.mobile ? "修改手机号" : "绑定手机号") : req.userInfo.email ? "修改邮箱" : "绑定邮箱",
|
||||||
|
wrapper: {
|
||||||
|
width: 560,
|
||||||
|
},
|
||||||
|
initialForm: {
|
||||||
|
phoneCode: req.userInfo.phoneCode || "86",
|
||||||
|
mobile: req.userInfo.mobile || "",
|
||||||
|
email: req.userInfo.email || "",
|
||||||
|
contactCaptcha: null,
|
||||||
|
contactValidateCode: "",
|
||||||
|
},
|
||||||
|
async onSubmit(form: any) {
|
||||||
|
if (isMobile) {
|
||||||
|
await userProfileApi.UpdateMobile({
|
||||||
|
phoneCode: form.phoneCode,
|
||||||
|
mobile: form.mobile,
|
||||||
|
validateCode: form.contactValidateCode,
|
||||||
|
identityValidationCode,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await userProfileApi.UpdateEmail({
|
||||||
|
email: form.email,
|
||||||
|
validateCode: form.contactValidateCode,
|
||||||
|
identityValidationCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
message.success("绑定信息已更新");
|
||||||
|
await req.onUpdated?.();
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
phoneCode: {
|
||||||
|
title: "区号",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
show: isMobile,
|
||||||
|
component: {
|
||||||
|
placeholder: "区号",
|
||||||
|
},
|
||||||
|
rules: [{ required: isMobile, message: "请输入区号" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
title: "手机号",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
show: isMobile,
|
||||||
|
component: {
|
||||||
|
placeholder: "请输入手机号",
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{ required: isMobile, message: "请输入手机号" },
|
||||||
|
{ pattern: /^\d{4,20}$/, message: "请输入正确的手机号" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
title: "邮箱",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
show: !isMobile,
|
||||||
|
component: {
|
||||||
|
placeholder: "请输入邮箱",
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{ required: !isMobile, message: "请输入邮箱" },
|
||||||
|
{ type: "email", message: "请输入正确的邮箱" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contactCaptcha: {
|
||||||
|
title: "图形验证码",
|
||||||
|
form: {
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
component: {
|
||||||
|
name: CaptchaInput,
|
||||||
|
vModel: "modelValue",
|
||||||
|
},
|
||||||
|
rules: [{ required: true, message: "请完成图形验证码" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contactValidateCode: {
|
||||||
|
title: isMobile ? "新手机号验证码" : "新邮箱验证码",
|
||||||
|
form: {
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
component: {
|
||||||
|
name: ContactCodeInput,
|
||||||
|
vModel: "modelValue",
|
||||||
|
form: compute(({ form }) => form),
|
||||||
|
type: req.type,
|
||||||
|
},
|
||||||
|
rules: [{ required: true, message: "请输入验证码" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await openFormDialog({
|
||||||
|
title: "验证本人操作",
|
||||||
|
wrapper: {
|
||||||
|
width: 520,
|
||||||
|
},
|
||||||
|
initialForm: {
|
||||||
|
identityType: "password",
|
||||||
|
identityPassword: "",
|
||||||
|
identityCaptcha: null,
|
||||||
|
identityValidateCode: "",
|
||||||
|
},
|
||||||
|
async onSubmit(form: any) {
|
||||||
|
const res = await userProfileApi.VerifyContactIdentity({
|
||||||
|
identityType: form.identityType,
|
||||||
|
identityPassword: form.identityPassword,
|
||||||
|
identityValidateCode: form.identityValidateCode,
|
||||||
|
});
|
||||||
|
await openChangeDialog(res.validationCode);
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
identityType: {
|
||||||
|
title: "验证方式",
|
||||||
|
form: {
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
component: {
|
||||||
|
name: "fs-dict-radio",
|
||||||
|
vModel: "value",
|
||||||
|
dict: dict({
|
||||||
|
data: methods,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
rules: [{ required: true, message: "请选择验证方式" }],
|
||||||
|
valueChange({ form }: { form: any }) {
|
||||||
|
form.identityPassword = "";
|
||||||
|
form.identityCaptcha = null;
|
||||||
|
form.identityValidateCode = "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
identityPassword: {
|
||||||
|
title: "登录密码",
|
||||||
|
type: "password",
|
||||||
|
form: {
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
show: compute(({ form }) => form.identityType === "password"),
|
||||||
|
component: {
|
||||||
|
placeholder: "请输入登录密码",
|
||||||
|
},
|
||||||
|
rules: [{ required: true, message: "请输入登录密码" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
identityCaptcha: {
|
||||||
|
title: "图形验证码",
|
||||||
|
form: {
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
show: compute(({ form }) => form.identityType !== "password"),
|
||||||
|
component: {
|
||||||
|
name: CaptchaInput,
|
||||||
|
vModel: "modelValue",
|
||||||
|
},
|
||||||
|
rules: [{ required: true, message: "请完成图形验证码" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
identityValidateCode: {
|
||||||
|
title: "验证码",
|
||||||
|
form: {
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
show: compute(({ form }) => form.identityType !== "password"),
|
||||||
|
component: {
|
||||||
|
name: IdentityCodeInput,
|
||||||
|
vModel: "modelValue",
|
||||||
|
form: compute(({ form }) => form),
|
||||||
|
userInfo: req.userInfo,
|
||||||
|
},
|
||||||
|
rules: [{ required: true, message: "请输入验证码" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
openContactBindDialog,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,13 +25,19 @@
|
|||||||
<span class="tag-icon">👤</span>
|
<span class="tag-icon">👤</span>
|
||||||
{{ userInfo.username }}
|
{{ userInfo.username }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<a-tag v-if="userInfo.email" color="green" class="detail-tag">
|
<a-tag color="green" class="detail-tag">
|
||||||
<span class="tag-icon">📧</span>
|
<span class="tag-icon">📧</span>
|
||||||
{{ userInfo.email }}
|
<span>{{ userInfo.email || "未绑定邮箱" }}</span>
|
||||||
|
<a-button type="text" size="small" class="detail-edit-btn" title="修改邮箱" @click.stop="openBindContact('email')">
|
||||||
|
<template #icon><fs-icon icon="ion:create-outline" /></template>
|
||||||
|
</a-button>
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<a-tag v-if="userInfo.mobile" color="purple" class="detail-tag">
|
<a-tag v-if="contactCapability.smsEnabled" color="purple" class="detail-tag">
|
||||||
<span class="tag-icon">📱</span>
|
<span class="tag-icon">📱</span>
|
||||||
{{ userInfo.mobile }}
|
<span>{{ userInfo.mobile || "未绑定手机号" }}</span>
|
||||||
|
<a-button type="text" size="small" class="detail-edit-btn" title="修改手机号" @click.stop="openBindContact('mobile')">
|
||||||
|
<template #icon><fs-icon icon="ion:create-outline" /></template>
|
||||||
|
</a-button>
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,7 +145,7 @@ import * as api from "./api";
|
|||||||
import { computed, onMounted, Ref, ref } from "vue";
|
import { computed, onMounted, Ref, ref } from "vue";
|
||||||
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
|
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
|
||||||
import { useI18n } from "/src/locales";
|
import { useI18n } from "/src/locales";
|
||||||
import { useUserProfile } from "./use";
|
import { useContactBind, useUserProfile } from "./use";
|
||||||
import { usePasskeyRegister } from "./use";
|
import { usePasskeyRegister } from "./use";
|
||||||
import { message, Modal, notification } from "ant-design-vue";
|
import { message, Modal, notification } from "ant-design-vue";
|
||||||
import { useSettingStore } from "/@/store/settings";
|
import { useSettingStore } from "/@/store/settings";
|
||||||
@@ -160,6 +166,9 @@ const settingStore = useSettingStore();
|
|||||||
const userInfo: Ref = ref({});
|
const userInfo: Ref = ref({});
|
||||||
const passkeys = ref([]);
|
const passkeys = ref([]);
|
||||||
const passkeySupported = ref(false);
|
const passkeySupported = ref(false);
|
||||||
|
const contactCapability = ref({
|
||||||
|
smsEnabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
const getUserInfo = async () => {
|
const getUserInfo = async () => {
|
||||||
userInfo.value = await api.getMineInfo();
|
userInfo.value = await api.getMineInfo();
|
||||||
@@ -177,6 +186,7 @@ function doUpdate() {
|
|||||||
openEditProfileDialog({
|
openEditProfileDialog({
|
||||||
onUpdated: async () => {
|
onUpdated: async () => {
|
||||||
await getUserInfo();
|
await getUserInfo();
|
||||||
|
userStore.setUserInfo(userInfo.value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -237,6 +247,23 @@ async function loadPasskeys() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadContactCapability() {
|
||||||
|
contactCapability.value = await api.GetContactCapability();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { openContactBindDialog } = useContactBind();
|
||||||
|
async function openBindContact(type: "mobile" | "email") {
|
||||||
|
await openContactBindDialog({
|
||||||
|
type,
|
||||||
|
userInfo: userInfo.value,
|
||||||
|
contactCapability: contactCapability.value,
|
||||||
|
onUpdated: async () => {
|
||||||
|
await getUserInfo();
|
||||||
|
userStore.setUserInfo(userInfo.value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function unbindPasskey(id: number) {
|
async function unbindPasskey(id: number) {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: "确认解绑吗?",
|
title: "确认解绑吗?",
|
||||||
@@ -366,6 +393,7 @@ const userAvatar = computed(() => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getUserInfo();
|
await getUserInfo();
|
||||||
|
await loadContactCapability();
|
||||||
await loadOauthBounds();
|
await loadOauthBounds();
|
||||||
await loadOauthProviders();
|
await loadOauthProviders();
|
||||||
await loadPasskeys();
|
await loadPasskeys();
|
||||||
@@ -613,6 +641,18 @@ onMounted(async () => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-edit-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
margin: -2px -6px -2px 0;
|
||||||
|
padding: 0;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
.tag-icon {
|
.tag-icon {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { BaseController, Constants } from '@certd/lib-server';
|
import { BaseController, Constants, SysSettingsService } from '@certd/lib-server';
|
||||||
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
|
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
|
||||||
import { PasskeyService } from '../../../modules/login/service/passkey-service.js';
|
import { PasskeyService } from '../../../modules/login/service/passkey-service.js';
|
||||||
import { RoleService } from '../../../modules/sys/authority/service/role-service.js';
|
import { RoleService } from '../../../modules/sys/authority/service/role-service.js';
|
||||||
import { UserService } from '../../../modules/sys/authority/service/user-service.js';
|
import { UserService } from '../../../modules/sys/authority/service/user-service.js';
|
||||||
import { ApiTags } from '@midwayjs/swagger';
|
import { ApiTags } from '@midwayjs/swagger';
|
||||||
|
import { CodeService } from '../../../modules/basic/service/code-service.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
@@ -20,8 +21,13 @@ export class MineController extends BaseController {
|
|||||||
@Inject()
|
@Inject()
|
||||||
passkeyService: PasskeyService;
|
passkeyService: PasskeyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
codeService: CodeService;
|
||||||
|
|
||||||
@Post('/info', { description: Constants.per.authOnly, summary: "查询用户信息" })
|
@Inject()
|
||||||
|
sysSettingsService: SysSettingsService;
|
||||||
|
|
||||||
|
@Post('/info', { description: Constants.per.authOnly, summary: '查询用户信息' })
|
||||||
public async info() {
|
public async info() {
|
||||||
const userId = this.getUserId();
|
const userId = this.getUserId();
|
||||||
const user = await this.userService.info(userId);
|
const user = await this.userService.info(userId);
|
||||||
@@ -35,21 +41,75 @@ export class MineController extends BaseController {
|
|||||||
return this.ok(user);
|
return this.ok(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/changePassword', { description: Constants.per.authOnly, summary: "修改密码" })
|
@Post('/changePassword', { description: Constants.per.authOnly, summary: '修改密码' })
|
||||||
public async changePassword(@Body(ALL) body: any) {
|
public async changePassword(@Body(ALL) body: any) {
|
||||||
const userId = this.getUserId();
|
const userId = this.getUserId();
|
||||||
await this.userService.changePassword(userId, body);
|
await this.userService.changePassword(userId, body);
|
||||||
return this.ok({});
|
return this.ok({});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/updateProfile', { description: Constants.per.authOnly, summary: "更新用户资料" })
|
@Post('/updateProfile', { description: Constants.per.authOnly, summary: '更新用户资料' })
|
||||||
public async updateProfile(@Body(ALL) body: any) {
|
public async updateProfile(@Body(ALL) body: any) {
|
||||||
const userId = this.getUserId();
|
const userId = this.getUserId();
|
||||||
|
|
||||||
await this.userService.updateProfile(userId, {
|
await this.userService.updateProfile(userId, {
|
||||||
avatar: body.avatar,
|
avatar: body.avatar,
|
||||||
nickName: body.nickName,
|
nickName: body.nickName,
|
||||||
});
|
});
|
||||||
return this.ok({});
|
return this.ok({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/contact/capability', { description: Constants.per.authOnly, summary: '查询联系方式绑定能力' })
|
||||||
|
public async contactCapability() {
|
||||||
|
const settings = await this.sysSettingsService.getPrivateSettings();
|
||||||
|
return this.ok({
|
||||||
|
smsEnabled: !!settings.sms?.config?.accessId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/contact/verifyIdentity', { description: Constants.per.authOnly, summary: '验证本人操作' })
|
||||||
|
public async verifyContactIdentity(@Body(ALL) body: { identityType: 'password' | 'email' | 'mobile'; identityPassword?: string; identityValidateCode?: string }) {
|
||||||
|
const userId = this.getUserId();
|
||||||
|
await this.userService.verifyIdentity(userId, body, this.codeService);
|
||||||
|
const validationCode = this.codeService.setValidationValue({
|
||||||
|
type: 'contactIdentity',
|
||||||
|
userId,
|
||||||
|
identityType: body.identityType,
|
||||||
|
});
|
||||||
|
return this.ok({ validationCode });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/contact/mobile', { description: Constants.per.authOnly, summary: '绑定或修改手机号' })
|
||||||
|
public async updateMobile(@Body(ALL) body: { phoneCode?: string; mobile: string; validateCode: string; identityValidationCode: string }) {
|
||||||
|
const userId = this.getUserId();
|
||||||
|
this.userService.checkContactIdentityValidation(userId, body.identityValidationCode, this.codeService);
|
||||||
|
await this.codeService.checkSmsCode({
|
||||||
|
mobile: body.mobile,
|
||||||
|
phoneCode: body.phoneCode || '86',
|
||||||
|
smsCode: body.validateCode,
|
||||||
|
verificationType: 'bindMobile',
|
||||||
|
throwError: true,
|
||||||
|
});
|
||||||
|
await this.userService.updateMobile(userId, {
|
||||||
|
phoneCode: body.phoneCode,
|
||||||
|
mobile: body.mobile,
|
||||||
|
});
|
||||||
|
return this.ok({});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/contact/email', { description: Constants.per.authOnly, summary: '绑定或修改邮箱' })
|
||||||
|
public async updateEmail(@Body(ALL) body: { email: string; validateCode: string; identityValidationCode: string }) {
|
||||||
|
const userId = this.getUserId();
|
||||||
|
this.userService.checkContactIdentityValidation(userId, body.identityValidationCode, this.codeService);
|
||||||
|
this.codeService.checkEmailCode({
|
||||||
|
email: body.email,
|
||||||
|
validateCode: body.validateCode,
|
||||||
|
verificationType: 'bindEmail',
|
||||||
|
throwError: true,
|
||||||
|
});
|
||||||
|
await this.userService.updateEmail(userId, {
|
||||||
|
email: body.email,
|
||||||
|
});
|
||||||
|
return this.ok({});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/// <reference types="mocha" />
|
||||||
|
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { Not } from 'typeorm';
|
||||||
|
import { buildUserContactConflictWhere } from './user-service.js';
|
||||||
|
|
||||||
|
describe('buildUserContactConflictWhere', () => {
|
||||||
|
it('checks username, mobile and email conflicts except current user', () => {
|
||||||
|
const where = buildUserContactConflictWhere('user@example.com', 12);
|
||||||
|
|
||||||
|
assert.deepEqual(where, [
|
||||||
|
{ username: 'user@example.com', id: Not(12) },
|
||||||
|
{ mobile: 'user@example.com', id: Not(12) },
|
||||||
|
{ email: 'user@example.com', id: Not(12) },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims contact value before building conflict query', () => {
|
||||||
|
const where = buildUserContactConflictWhere(' 13800138000 ', 3);
|
||||||
|
|
||||||
|
assert.deepEqual(where, [
|
||||||
|
{ username: '13800138000', id: Not(3) },
|
||||||
|
{ mobile: '13800138000', id: Not(3) },
|
||||||
|
{ email: '13800138000', id: Not(3) },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import {EntityManager, In, MoreThan, Not, Repository} from 'typeorm';
|
import { EntityManager, In, MoreThan, Not, Repository } from 'typeorm';
|
||||||
import { UserEntity } from '../entity/user.js';
|
import { UserEntity } from '../entity/user.js';
|
||||||
import * as _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import { BaseService, CommonException, Constants, FileService, SysInstallInfo, SysSettingsService } from '@certd/lib-server';
|
import { BaseService, CommonException, Constants, FileService, SysInstallInfo, SysSettingsService } from '@certd/lib-server';
|
||||||
@@ -18,15 +18,22 @@ import { OauthBoundService } from '../../../login/service/oauth-bound-service.js
|
|||||||
export type RegisterType = 'username' | 'mobile' | 'email';
|
export type RegisterType = 'username' | 'mobile' | 'email';
|
||||||
export type ForgotPasswordType = 'mobile' | 'email';
|
export type ForgotPasswordType = 'mobile' | 'email';
|
||||||
|
|
||||||
export const AdminRoleId = 1
|
export const AdminRoleId = 1;
|
||||||
|
|
||||||
|
export function buildUserContactConflictWhere(value: string, userId: number) {
|
||||||
|
const contact = value?.trim();
|
||||||
|
return [
|
||||||
|
{ username: contact, id: Not(userId) },
|
||||||
|
{ mobile: contact, id: Not(userId) },
|
||||||
|
{ email: contact, id: Not(userId) },
|
||||||
|
];
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 系统用户
|
* 系统用户
|
||||||
*/
|
*/
|
||||||
@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()
|
||||||
@@ -44,10 +51,9 @@ export class UserService extends BaseService<UserEntity> {
|
|||||||
@Inject()
|
@Inject()
|
||||||
dbAdapter: DbAdapter;
|
dbAdapter: DbAdapter;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
oauthBoundService: OauthBoundService;
|
oauthBoundService: OauthBoundService;
|
||||||
|
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
getRepository() {
|
getRepository() {
|
||||||
return this.repository;
|
return this.repository;
|
||||||
@@ -145,7 +151,7 @@ export class UserService extends BaseService<UserEntity> {
|
|||||||
return bcrypt.hashSync(plainPassword, salt);
|
return bcrypt.hashSync(plainPassword, salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(param: Record<string,any>) {
|
async findOne(param: Record<string, any>) {
|
||||||
return this.repository.findOne({
|
return this.repository.findOne({
|
||||||
where: param,
|
where: param,
|
||||||
});
|
});
|
||||||
@@ -177,12 +183,11 @@ export class UserService extends BaseService<UserEntity> {
|
|||||||
return await this.roleService.getPermissionByRoleIds(roleIds);
|
return await this.roleService.getPermissionByRoleIds(roleIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(type: string, user: UserEntity,withTx?:(tx: EntityManager)=>Promise<void>) {
|
async register(type: string, user: UserEntity, withTx?: (tx: EntityManager) => Promise<void>) {
|
||||||
if (!user.password) {
|
if (!user.password) {
|
||||||
user.password = simpleNanoId();
|
user.password = simpleNanoId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (user.username) {
|
if (user.username) {
|
||||||
const username = user.username;
|
const username = user.username;
|
||||||
const old = await this.findOne([{ username: username }, { mobile: username }, { email: username }]);
|
const old = await this.findOne([{ username: username }, { mobile: username }, { email: username }]);
|
||||||
@@ -208,7 +213,6 @@ export class UserService extends BaseService<UserEntity> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!user.username) {
|
if (!user.username) {
|
||||||
user.username = 'user_' + simpleNanoId();
|
user.username = 'user_' + simpleNanoId();
|
||||||
}
|
}
|
||||||
@@ -235,7 +239,7 @@ export class UserService extends BaseService<UserEntity> {
|
|||||||
const userRole: UserRoleEntity = UserRoleEntity.of(newUser.id, Constants.role.defaultUser);
|
const userRole: UserRoleEntity = UserRoleEntity.of(newUser.id, Constants.role.defaultUser);
|
||||||
await txManager.save(userRole);
|
await txManager.save(userRole);
|
||||||
|
|
||||||
if(withTx) {
|
if (withTx) {
|
||||||
await withTx(txManager);
|
await withTx(txManager);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -247,35 +251,26 @@ export class UserService extends BaseService<UserEntity> {
|
|||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
async forgotPassword(
|
async forgotPassword(data: { type: ForgotPasswordType; input?: string; phoneCode?: string; validateCode: string; password: string; confirmPassword: string }) {
|
||||||
data: {
|
if (!data.type) {
|
||||||
type: ForgotPasswordType;
|
|
||||||
input?: string,
|
|
||||||
phoneCode?: string,
|
|
||||||
validateCode: string,
|
|
||||||
password: string,
|
|
||||||
confirmPassword: string,
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
if(!data.type) {
|
|
||||||
throw new CommonException('找回类型不能为空');
|
throw new CommonException('找回类型不能为空');
|
||||||
}
|
}
|
||||||
if(data.password !== data.confirmPassword) {
|
if (data.password !== data.confirmPassword) {
|
||||||
throw new CommonException('两次输入的密码不一致');
|
throw new CommonException('两次输入的密码不一致');
|
||||||
}
|
}
|
||||||
const where :any= {
|
const where: any = {
|
||||||
[data.type]: data.input,
|
[data.type]: data.input,
|
||||||
};
|
};
|
||||||
if (data.type === 'mobile' ) {
|
if (data.type === 'mobile') {
|
||||||
where.phoneCode = data.phoneCode ?? '86';
|
where.phoneCode = data.phoneCode ?? '86';
|
||||||
}
|
}
|
||||||
const user = await this.findOne({ [data.type]: data.input });
|
const user = await this.findOne({ [data.type]: data.input });
|
||||||
console.log('user', user)
|
console.log('user', user);
|
||||||
if(!user) {
|
if (!user) {
|
||||||
throw new CommonException('用户不存在');
|
throw new CommonException('用户不存在');
|
||||||
// return;
|
// return;
|
||||||
}
|
}
|
||||||
await this.resetPassword(user.id, data.password)
|
await this.resetPassword(user.id, data.password);
|
||||||
return user.username;
|
return user.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,30 +371,102 @@ export class UserService extends BaseService<UserEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAdmins() {
|
async getAdmins() {
|
||||||
const admins = await this.userRoleService.find({
|
const admins = await this.userRoleService.find({
|
||||||
where: {
|
where: {
|
||||||
roleId: AdminRoleId,
|
roleId: AdminRoleId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const userIds = admins.map(item => item.userId);
|
const userIds = admins.map(item => item.userId);
|
||||||
return await this.repository.find({
|
return await this.repository.find({
|
||||||
where: {
|
where: {
|
||||||
id: In(userIds),
|
id: In(userIds),
|
||||||
status: 1,
|
status: 1,
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
updateTime: 'DESC',
|
updateTime: 'DESC',
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProfile(userId: any, body: any) {
|
async updateProfile(userId: any, body: any) {
|
||||||
|
|
||||||
await this.update({
|
await this.update({
|
||||||
id: userId,
|
id: userId,
|
||||||
...body,
|
...body,
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyIdentity(userId: number, body: { identityType: 'password' | 'email' | 'mobile'; identityPassword?: string; identityValidateCode?: string }, codeService: any) {
|
||||||
|
const user = await this.info(userId);
|
||||||
|
if (body.identityType === 'password') {
|
||||||
|
const passwordChecked = await this.checkPassword(body.identityPassword, user.password, user.passwordVersion);
|
||||||
|
if (!passwordChecked) {
|
||||||
|
throw new CommonException('密码错误');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (body.identityType === 'email') {
|
||||||
|
if (!user.email) {
|
||||||
|
throw new CommonException('当前账号未绑定邮箱');
|
||||||
|
}
|
||||||
|
codeService.checkEmailCode({
|
||||||
|
email: user.email,
|
||||||
|
validateCode: body.identityValidateCode,
|
||||||
|
verificationType: 'contactIdentity',
|
||||||
|
throwError: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (body.identityType === 'mobile') {
|
||||||
|
if (!user.mobile) {
|
||||||
|
throw new CommonException('当前账号未绑定手机号');
|
||||||
|
}
|
||||||
|
await codeService.checkSmsCode({
|
||||||
|
mobile: user.mobile,
|
||||||
|
phoneCode: user.phoneCode || '86',
|
||||||
|
smsCode: body.identityValidateCode,
|
||||||
|
verificationType: 'contactIdentity',
|
||||||
|
throwError: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new CommonException('不支持的验证方式');
|
||||||
|
}
|
||||||
|
|
||||||
|
checkContactIdentityValidation(userId: number, validationCode: string, codeService: any) {
|
||||||
|
const validationValue = codeService.getValidationValue(validationCode);
|
||||||
|
if (!validationValue || validationValue.type !== 'contactIdentity' || validationValue.userId !== userId) {
|
||||||
|
throw new CommonException('请先验证本人操作');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateMobile(userId: number, body: { phoneCode?: string; mobile: string }) {
|
||||||
|
const mobile = body.mobile?.trim();
|
||||||
|
if (!mobile) {
|
||||||
|
throw new CommonException('手机号不能为空');
|
||||||
|
}
|
||||||
|
const old = await this.findOne(buildUserContactConflictWhere(mobile, userId));
|
||||||
|
if (old != null) {
|
||||||
|
throw new CommonException('手机号已被占用');
|
||||||
|
}
|
||||||
|
await this.repository.update(userId, {
|
||||||
|
phoneCode: body.phoneCode || '86',
|
||||||
|
mobile,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEmail(userId: number, body: { email: string }) {
|
||||||
|
const email = body.email?.trim();
|
||||||
|
if (!email) {
|
||||||
|
throw new CommonException('邮箱不能为空');
|
||||||
|
}
|
||||||
|
const old = await this.findOne(buildUserContactConflictWhere(email, userId));
|
||||||
|
if (old != null) {
|
||||||
|
throw new CommonException('邮箱已被占用');
|
||||||
|
}
|
||||||
|
await this.repository.update(userId, {
|
||||||
|
email,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllUserIds() {
|
async getAllUserIds() {
|
||||||
@@ -408,7 +475,7 @@ export class UserService extends BaseService<UserEntity> {
|
|||||||
where: {
|
where: {
|
||||||
status: 1,
|
status: 1,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
return users.map(item => item.id);
|
return users.map(item => item.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user