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); } }