diff --git a/packages/ui/certd-client/src/views/certd/mine/api.ts b/packages/ui/certd-client/src/views/certd/mine/api.ts
index 9ce8648ad..c30fb7d78 100644
--- a/packages/ui/certd-client/src/views/certd/mine/api.ts
+++ b/packages/ui/certd-client/src/views/certd/mine/api.ts
@@ -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() {
return await request({
url: "/oauth/bounds",
diff --git a/packages/ui/certd-client/src/views/certd/mine/contact-code-input.tsx b/packages/ui/certd-client/src/views/certd/mine/contact-code-input.tsx
new file mode 100644
index 000000000..c1e5cef84
--- /dev/null
+++ b/packages/ui/certd-client/src/views/certd/mine/contact-code-input.tsx
@@ -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 ;
+ }
+ return ;
+ };
+ },
+});
diff --git a/packages/ui/certd-client/src/views/certd/mine/identity-code-input.tsx b/packages/ui/certd-client/src/views/certd/mine/identity-code-input.tsx
new file mode 100644
index 000000000..9fde09b17
--- /dev/null
+++ b/packages/ui/certd-client/src/views/certd/mine/identity-code-input.tsx
@@ -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 ;
+ }
+ return (
+
+ );
+ };
+ },
+});
diff --git a/packages/ui/certd-client/src/views/certd/mine/use.tsx b/packages/ui/certd-client/src/views/certd/mine/use.tsx
index f5e4e536e..d95b37ff0 100644
--- a/packages/ui/certd-client/src/views/certd/mine/use.tsx
+++ b/packages/ui/certd-client/src/views/certd/mine/use.tsx
@@ -1,20 +1,22 @@
// useUserProfile, 获取 openEditProfileDialog ,参考 useTemplate方法
-import { useFormWrapper } from "@fast-crud/fast-crud";
-import { ref } from "vue";
-import { cloneDeep, merge } from "lodash-es";
+import { compute, dict } from "@fast-crud/fast-crud";
// 假设的 API 导入
import * as userProfileApi from "./api";
import { useUserStore } from "/@/store/user";
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}}
*/
export function useUserProfile() {
- const { openCrudFormDialog } = useFormWrapper();
- const wrapperRef = ref();
+ const { openFormDialog } = useFormDialog();
async function openEditProfileDialog(req: { onUpdated?: (ctx: any) => void }) {
const detail = await userProfileApi.getMineInfo();
if (!detail) {
@@ -24,31 +26,28 @@ export function useUserProfile() {
const { t } = useI18n();
const userStore = useUserStore();
- const userProfileFormRef = ref();
- async function doSubmit(opts: { form: any }) {
- const form = opts.form;
+ async function doSubmit(form: any) {
const { id } = await userProfileApi.UpdateProfile(form);
if (req.onUpdated) {
req.onUpdated({ id });
}
}
- const crudOptions: any = {
- form: {
- doSubmit,
- wrapper: {
- title: `编辑用户资料`,
- width: 1100,
- onOpened(opts: { form: any }) {
- merge(opts.form, detail);
- },
- },
+ await openFormDialog({
+ title: `编辑用户资料`,
+ wrapper: {
+ width: 600,
},
+ initialForm: detail,
+ onSubmit: doSubmit,
columns: {
nickName: {
title: t("certd.nickName"),
type: "text",
form: {
+ col: {
+ span: 24,
+ },
component: {
placeholder: t("certd.nickName"),
},
@@ -71,6 +70,9 @@ export function useUserProfile() {
},
},
form: {
+ col: {
+ span: 24,
+ },
component: {
vModel: "modelValue",
valueType: "key",
@@ -98,10 +100,7 @@ export function useUserProfile() {
},
},
},
- };
-
- const wrapper = await openCrudFormDialog({ crudOptions });
- wrapperRef.value = wrapper;
+ });
}
return {
@@ -110,26 +109,20 @@ export function useUserProfile() {
}
export function usePasskeyRegister() {
- const { openCrudFormDialog } = useFormWrapper();
- const wrapperRef = ref();
+ const { openFormDialog } = useFormDialog();
async function openRegisterDialog(req: { onSubmit?: (ctx: any) => void }) {
const { t } = useI18n();
- const userStore = useUserStore();
- const deviceNameRef = ref();
-
- const crudOptions: any = {
- form: {
- wrapper: {
- title: t("authentication.registerPasskey"),
- width: 500,
- onOpened(opts: { form: any }) {
- opts.form.deviceName = "";
- },
- },
- onSubmit: req.onSubmit,
- afterSubmit: null,
- onSuccess: null,
+ await openFormDialog({
+ title: t("authentication.registerPasskey"),
+ wrapper: {
+ width: 500,
+ },
+ initialForm: {
+ deviceName: "",
+ },
+ onSubmit: async (form: any) => {
+ await req.onSubmit?.({ form });
},
columns: {
deviceName: {
@@ -147,15 +140,229 @@ export function usePasskeyRegister() {
},
},
},
- };
-
- const wrapper = await openCrudFormDialog({ crudOptions });
- wrapperRef.value = wrapper;
-
- return wrapper;
+ });
}
return {
openRegisterDialog,
};
}
+
+export function useContactBind() {
+ const { openFormDialog } = useFormDialog();
+
+ async function openContactBindDialog(req: { type: "mobile" | "email"; userInfo: any; contactCapability: { smsEnabled?: boolean }; onUpdated?: () => Promise | 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,
+ };
+}
diff --git a/packages/ui/certd-client/src/views/certd/mine/user-profile.vue b/packages/ui/certd-client/src/views/certd/mine/user-profile.vue
index 290fc0a4e..ac115d997 100644
--- a/packages/ui/certd-client/src/views/certd/mine/user-profile.vue
+++ b/packages/ui/certd-client/src/views/certd/mine/user-profile.vue
@@ -25,13 +25,19 @@
👤
{{ userInfo.username }}
-
+
📧
- {{ userInfo.email }}
+ {{ userInfo.email || "未绑定邮箱" }}
+
+
+
-
+
📱
- {{ userInfo.mobile }}
+ {{ userInfo.mobile || "未绑定手机号" }}
+
+
+
@@ -139,7 +145,7 @@ import * as api from "./api";
import { computed, onMounted, Ref, ref } from "vue";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { useI18n } from "/src/locales";
-import { useUserProfile } from "./use";
+import { useContactBind, useUserProfile } from "./use";
import { usePasskeyRegister } from "./use";
import { message, Modal, notification } from "ant-design-vue";
import { useSettingStore } from "/@/store/settings";
@@ -160,6 +166,9 @@ const settingStore = useSettingStore();
const userInfo: Ref = ref({});
const passkeys = ref([]);
const passkeySupported = ref(false);
+const contactCapability = ref({
+ smsEnabled: false,
+});
const getUserInfo = async () => {
userInfo.value = await api.getMineInfo();
@@ -177,6 +186,7 @@ function doUpdate() {
openEditProfileDialog({
onUpdated: async () => {
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) {
Modal.confirm({
title: "确认解绑吗?",
@@ -366,6 +393,7 @@ const userAvatar = computed(() => {
onMounted(async () => {
await getUserInfo();
+ await loadContactCapability();
await loadOauthBounds();
await loadOauthProviders();
await loadPasskeys();
@@ -613,6 +641,18 @@ onMounted(async () => {
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 {
font-size: 14px;
}
diff --git a/packages/ui/certd-server/src/controller/user/mine/mine-controller.ts b/packages/ui/certd-server/src/controller/user/mine/mine-controller.ts
index d61438eec..95f7c96da 100644
--- a/packages/ui/certd-server/src/controller/user/mine/mine-controller.ts
+++ b/packages/ui/certd-server/src/controller/user/mine/mine-controller.ts
@@ -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 { PasskeyService } from '../../../modules/login/service/passkey-service.js';
import { RoleService } from '../../../modules/sys/authority/service/role-service.js';
import { UserService } from '../../../modules/sys/authority/service/user-service.js';
import { ApiTags } from '@midwayjs/swagger';
+import { CodeService } from '../../../modules/basic/service/code-service.js';
/**
*/
@@ -20,8 +21,13 @@ export class MineController extends BaseController {
@Inject()
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() {
const userId = this.getUserId();
const user = await this.userService.info(userId);
@@ -35,21 +41,75 @@ export class MineController extends BaseController {
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) {
const userId = this.getUserId();
await this.userService.changePassword(userId, body);
return this.ok({});
}
- @Post('/updateProfile', { description: Constants.per.authOnly, summary: "更新用户资料" })
+ @Post('/updateProfile', { description: Constants.per.authOnly, summary: '更新用户资料' })
public async updateProfile(@Body(ALL) body: any) {
const userId = this.getUserId();
-
+
await this.userService.updateProfile(userId, {
avatar: body.avatar,
nickName: body.nickName,
});
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({});
+ }
}
diff --git a/packages/ui/certd-server/src/modules/sys/authority/service/user-contact.test.ts b/packages/ui/certd-server/src/modules/sys/authority/service/user-contact.test.ts
new file mode 100644
index 000000000..ce0f6818a
--- /dev/null
+++ b/packages/ui/certd-server/src/modules/sys/authority/service/user-contact.test.ts
@@ -0,0 +1,27 @@
+///
+
+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) },
+ ]);
+ });
+});
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 9015040d9..750f2d769 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
@@ -1,6 +1,6 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
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 * as _ from 'lodash-es';
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 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()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class UserService extends BaseService {
-
-
@InjectEntityModel(UserEntity)
repository: Repository;
@Inject()
@@ -44,10 +51,9 @@ export class UserService extends BaseService {
@Inject()
dbAdapter: DbAdapter;
- @Inject()
+ @Inject()
oauthBoundService: OauthBoundService;
-
//@ts-ignore
getRepository() {
return this.repository;
@@ -145,7 +151,7 @@ export class UserService extends BaseService {
return bcrypt.hashSync(plainPassword, salt);
}
- async findOne(param: Record) {
+ async findOne(param: Record) {
return this.repository.findOne({
where: param,
});
@@ -177,12 +183,11 @@ export class UserService extends BaseService {
return await this.roleService.getPermissionByRoleIds(roleIds);
}
- async register(type: string, user: UserEntity,withTx?:(tx: EntityManager)=>Promise) {
+ async register(type: string, user: UserEntity, withTx?: (tx: EntityManager) => Promise) {
if (!user.password) {
user.password = simpleNanoId();
}
-
if (user.username) {
const username = user.username;
const old = await this.findOne([{ username: username }, { mobile: username }, { email: username }]);
@@ -208,7 +213,6 @@ export class UserService extends BaseService {
}
}
-
if (!user.username) {
user.username = 'user_' + simpleNanoId();
}
@@ -235,7 +239,7 @@ export class UserService extends BaseService {
const userRole: UserRoleEntity = UserRoleEntity.of(newUser.id, Constants.role.defaultUser);
await txManager.save(userRole);
- if(withTx) {
+ if (withTx) {
await withTx(txManager);
}
});
@@ -247,35 +251,26 @@ export class UserService extends BaseService {
return newUser;
}
- async forgotPassword(
- data: {
- type: ForgotPasswordType;
- input?: string,
- phoneCode?: string,
- validateCode: string,
- password: string,
- confirmPassword: string,
- }
- ) {
- if(!data.type) {
+ async forgotPassword(data: { type: ForgotPasswordType; input?: string; phoneCode?: string; validateCode: string; password: string; confirmPassword: string }) {
+ if (!data.type) {
throw new CommonException('找回类型不能为空');
}
- if(data.password !== data.confirmPassword) {
+ if (data.password !== data.confirmPassword) {
throw new CommonException('两次输入的密码不一致');
}
- const where :any= {
+ const where: any = {
[data.type]: data.input,
};
- if (data.type === 'mobile' ) {
- where.phoneCode = data.phoneCode ?? '86';
+ if (data.type === 'mobile') {
+ where.phoneCode = data.phoneCode ?? '86';
}
const user = await this.findOne({ [data.type]: data.input });
- console.log('user', user)
- if(!user) {
+ console.log('user', user);
+ if (!user) {
throw new CommonException('用户不存在');
// return;
}
- await this.resetPassword(user.id, data.password)
+ await this.resetPassword(user.id, data.password);
return user.username;
}
@@ -376,30 +371,102 @@ export class UserService extends BaseService {
}
async getAdmins() {
- const admins = await this.userRoleService.find({
- where: {
- roleId: AdminRoleId,
- },
- });
+ const admins = await this.userRoleService.find({
+ where: {
+ roleId: AdminRoleId,
+ },
+ });
- const userIds = admins.map(item => item.userId);
- return await this.repository.find({
- where: {
- id: In(userIds),
- status: 1,
- },
- order: {
- updateTime: 'DESC',
- },
- })
+ const userIds = admins.map(item => item.userId);
+ return await this.repository.find({
+ where: {
+ id: In(userIds),
+ status: 1,
+ },
+ order: {
+ updateTime: 'DESC',
+ },
+ });
}
async updateProfile(userId: any, body: any) {
-
await this.update({
id: userId,
...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() {
@@ -408,7 +475,7 @@ export class UserService extends BaseService {
where: {
status: 1,
},
- })
+ });
return users.map(item => item.id);
}
}